Skip to content

Commit f79c1ec

Browse files
authored
417 automatically pull bindings from bindings folder (#419)
* Remove old new scale, add const for bindings * Add platform info * Move Pinpoint ID to server * Migrated bindings * Generate GUI dynamically * Dynamic binding instancing * Updated binding documentation
1 parent e4543d3 commit f79c1ec

File tree

14 files changed

+189
-106
lines changed

14 files changed

+189
-106
lines changed

docs/development/adding_a_manipulator.md

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ read [how the system works first](../home/how_it_works.md) before proceeding.
1313
## Create a Manipulator Binding
1414

1515
Manipulators are added to Ephys Link through bindings. A binding is a Python class that extends the abstract base class
16-
[`BaseBinding`][ephys_link.utils.base_binding] and defines the functions Ephys Link expects from a platform.
16+
[`BaseBinding`][ephys_link.utils.base_binding] and defines the methods Ephys Link expects from a platform.
1717

1818
Create a new Python module in `src/ephys_link/bindings` for your manipulator. Make a class that extends
1919
[`BaseBinding`][ephys_link.utils.base_binding]. Most IDEs will automatically import the necessary classes and tell you
@@ -23,7 +23,7 @@ descriptions of the expected behavior.
2323
As described in the [system overview](../home/how_it_works.md), Ephys Link converts all manipulator movement into a
2424
common "unified space" which is
2525
the [left-hand cartesian coordinate system](https://www.scratchapixel.com/lessons/mathematics-physics-for-computer-graphics/geometry/coordinate-systems.html).
26-
The two functions [
26+
The two methods [
2727
`platform_space_to_unified_space`](../../reference/ephys_link/utils/base_binding/#ephys_link.utils.base_binding.BaseBinding.platform_space_to_unified_space)
2828
and [
2929
`unified_space_to_platform_space`](../../reference/ephys_link/utils/base_binding/#ephys_link.utils.base_binding.BaseBinding.unified_space_to_platform_space)
@@ -37,19 +37,29 @@ are used to convert between your manipulator's coordinate system and the unified
3737
the [New Scale Pathfinder MPM](https://github.com/VirtualBrainLab/ephys-link/blob/main/src/ephys_link/bindings/mpm_bindings.py)
3838
binding for an example where the platform uses a REST API to an external provider.
3939

40-
## Register the Binding
40+
### Binding Names
4141

42-
To make Ephys Link aware of your new binding, you'll need to register it in
43-
`src/ephys_link/back_end/platform_handler.py`. In the function [
44-
`_match_platform_type`](https://github.com/VirtualBrainLab/ephys-link/blob/c00be57bb552e5d0466b1cfebd0a54d555f12650/src/ephys_link/back_end/platform_handler.py#L69),
45-
add a new `case` to the `match` statement that returns an instance of your binding when matched to the desired CLI name
46-
for your platform. For example, to use Sensapex's uMp-4 the CLI launch command is `ephys_link.exe -b -t ump-4`,
47-
therefore the matching case statement is `ump-4`.
42+
The two naming methods [
43+
`get_display_name`](../../reference/ephys_link/utils/base_binding/#ephys_link.utils.base_binding.BaseBinding.get_display_name)
44+
and [
45+
`get_cli_name`](../../reference/ephys_link/utils/base_binding/#ephys_link.utils.base_binding.BaseBinding.get_cli_name)
46+
are used to identify the binding in the user interface. As described by their documentation, `get_display_name` should
47+
return a human-readable name for the binding, while `get_cli_name` should return the name used to launch the binding
48+
from the command line (what is passed to the `-t` flag). For example, Sensapex uMp-4 manipulator's `get_cli_name`
49+
returns `ump-4` because the CLI launch command is `ephys_link.exe -b -t ump-4`.
50+
51+
### Custom Additional Arguments
52+
53+
Sometimes you may want to pass extra data to your binding on initialization. For example, New Scale Pathfinder MPM
54+
bindings needs to know what the HTTP server port is. To add custom arguments, define them as arguments on the `__init__`
55+
method of your binding then pass in the appropriate data when the binding is instantiated in the [
56+
`_get_binding_instance`]() method of the [`PlatformHandler`][ephys_link.back_end.platform_handler]. Use [New Scale
57+
Pathfinder MPM's binding][ephys_link.bindings.mpm_binding] as an example for how to do this.
4858

4959
## Test Your Binding
5060

5161
Once you've implemented your binding, you can test it by running Ephys Link using your binding
52-
`ephys_link -b -t <your_manipulator>`. You can interact with it using the Socket.IO API or Pinpoint.
62+
`ephys_link -b -t <cli_name>`. You can interact with it using the [Socket.IO API](socketio_api.md) or Pinpoint.
5363

5464
## Submit Your Changes
5565

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ dependencies = [
3838
"requests==2.32.3",
3939
"sensapex==1.400.3",
4040
"rich==13.9.4",
41-
"vbl-aquarium==1.0.0b1"
41+
"vbl-aquarium==1.0.0b3"
4242
]
4343

4444
[project.urls]
@@ -80,5 +80,8 @@ dependencies = [
8080
serve = "mkdocs serve"
8181
build = "mkdocs build"
8282

83+
[tool.ruff]
84+
unsafe-fixes = true
85+
8386
[tool.ruff.lint]
8487
extend-ignore = ["DTZ005"]

src/ephys_link/back_end/platform_handler.py

Lines changed: 38 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
Responsible for performing the various manipulator commands.
55
Instantiates the appropriate bindings based on the platform type and uses them to perform the commands.
66
7-
Usage: Instantiate PlatformHandler with the platform type and call the desired command.
7+
Usage:
8+
Instantiate PlatformHandler with the platform type and call the desired command.
89
"""
910

1011
from uuid import uuid4
@@ -14,22 +15,18 @@
1415
BooleanStateResponse,
1516
EphysLinkOptions,
1617
GetManipulatorsResponse,
18+
PlatformInfo,
1719
PositionalResponse,
1820
SetDepthRequest,
1921
SetDepthResponse,
2022
SetInsideBrainRequest,
2123
SetPositionRequest,
2224
ShankCountResponse,
2325
)
24-
from vbl_aquarium.models.proxy import PinpointIdResponse
2526
from vbl_aquarium.models.unity import Vector4
2627

27-
from ephys_link.__about__ import __version__
28-
from ephys_link.bindings.fake_binding import FakeBinding
29-
from ephys_link.bindings.mpm_binding import MPMBinding
30-
from ephys_link.bindings.ump_4_binding import Ump4Binding
3128
from ephys_link.utils.base_binding import BaseBinding
32-
from ephys_link.utils.common import vector4_to_array
29+
from ephys_link.utils.common import get_bindings, vector4_to_array
3330
from ephys_link.utils.console import Console
3431

3532

@@ -50,83 +47,80 @@ def __init__(self, options: EphysLinkOptions, console: Console) -> None:
5047
self._console = console
5148

5249
# Define bindings based on platform type.
53-
self._bindings = self._match_platform_type(options)
50+
self._bindings = self._get_binding_instance(options)
5451

5552
# Record which IDs are inside the brain.
5653
self._inside_brain: set[str] = set()
5754

5855
# Generate a Pinpoint ID for proxy usage.
5956
self._pinpoint_id = str(uuid4())[:8]
6057

61-
def _match_platform_type(self, options: EphysLinkOptions) -> BaseBinding:
58+
def _get_binding_instance(self, options: EphysLinkOptions) -> BaseBinding:
6259
"""Match the platform type to the appropriate bindings.
6360
6461
Args:
6562
options: CLI options.
6663
64+
Raises:
65+
ValueError: If the platform type is not recognized.
66+
6767
Returns:
6868
Bindings for the specified platform type.
6969
"""
70-
match options.type:
71-
case "ump-4":
72-
return Ump4Binding()
73-
case "pathfinder-mpm":
74-
return MPMBinding(options.mpm_port)
75-
case "fake":
76-
return FakeBinding()
77-
case _:
78-
error_message = f'Platform type "{options.type}" not recognized.'
79-
self._console.critical_print(error_message)
80-
raise ValueError(error_message)
81-
82-
# Ephys Link metadata.
83-
84-
@staticmethod
85-
def get_version() -> str:
86-
"""Get Ephys Link's version.
70+
for binding_type in get_bindings():
71+
binding_cli_name = binding_type.get_cli_name()
8772

88-
Returns:
89-
Ephys Link's version.
90-
"""
91-
return __version__
73+
if binding_cli_name == options.type:
74+
# Pass in HTTP port for Pathfinder MPM.
75+
if binding_cli_name == "pathfinder-mpm":
76+
return binding_type(options.mpm_port)
77+
78+
# Otherwise just return the binding.
79+
return binding_type()
9280

93-
def get_pinpoint_id(self) -> PinpointIdResponse:
94-
"""Get the Pinpoint ID for proxy usage.
81+
# Raise an error if the platform type is not recognized.
82+
error_message = f'Platform type "{options.type}" not recognized.'
83+
self._console.critical_print(error_message)
84+
raise ValueError(error_message)
85+
86+
# Platform metadata.
87+
88+
def get_display_name(self) -> str:
89+
"""Get the display name for the platform.
9590
9691
Returns:
97-
Pinpoint ID response.
92+
Display name for the platform.
9893
"""
99-
return PinpointIdResponse(pinpoint_id=self._pinpoint_id, is_requester=False)
94+
return self._bindings.get_display_name()
10095

101-
def get_platform_type(self) -> str:
96+
async def get_platform_info(self) -> PlatformInfo:
10297
"""Get the manipulator platform type connected to Ephys Link.
10398
10499
Returns:
105100
Platform type config identifier (see CLI options for examples).
106101
"""
107-
return str(self._options.type)
102+
return PlatformInfo(
103+
name=self._bindings.get_display_name(),
104+
cli_name=self._bindings.get_cli_name(),
105+
axes_count=await self._bindings.get_axes_count(),
106+
dimensions=await self._bindings.get_dimensions(),
107+
)
108108

109109
# Manipulator commands.
110110

111111
async def get_manipulators(self) -> GetManipulatorsResponse:
112112
"""Get a list of available manipulators on the current handler.
113113
114114
Returns:
115-
List of manipulator IDs, number of axes, dimensions of manipulators (mm), and an error message if any.
115+
List of manipulator IDs or an error message if any.
116116
"""
117117
try:
118118
manipulators = await self._bindings.get_manipulators()
119-
num_axes = await self._bindings.get_axes_count()
120-
dimensions = self._bindings.get_dimensions()
121119
except Exception as e:
122120
self._console.exception_error_print("Get Manipulators", e)
123121
return GetManipulatorsResponse(error=self._console.pretty_exception(e))
124122
else:
125-
return GetManipulatorsResponse(
126-
manipulators=manipulators,
127-
num_axes=num_axes,
128-
dimensions=dimensions,
129-
)
123+
return GetManipulatorsResponse(manipulators=manipulators)
130124

131125
async def get_position(self, manipulator_id: str) -> PositionalResponse:
132126
"""Get the current translation position of a manipulator in unified coordinates (mm).

src/ephys_link/back_end/server.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,22 @@
1+
"""Socket.IO Server.
2+
3+
Responsible to managing the Socket.IO connection and events.
4+
Directs events to the platform handler or handles them directly.
5+
6+
Usage:
7+
Instantiate Server with the appropriate options, platform handler, and console.
8+
Then call `launch()` to start the server.
9+
10+
```python
11+
Server(options, platform_handler, console).launch()
12+
```
13+
"""
14+
115
from asyncio import get_event_loop, run
216
from collections.abc import Callable, Coroutine
317
from json import JSONDecodeError, dumps, loads
418
from typing import Any
19+
from uuid import uuid4
520

621
from aiohttp.web import Application, run_app
722
from pydantic import ValidationError
@@ -12,8 +27,10 @@
1227
SetInsideBrainRequest,
1328
SetPositionRequest,
1429
)
30+
from vbl_aquarium.models.proxy import PinpointIdResponse
1531
from vbl_aquarium.utils.vbl_base_model import VBLBaseModel
1632

33+
from ephys_link.__about__ import __version__
1734
from ephys_link.back_end.platform_handler import PlatformHandler
1835
from ephys_link.utils.common import PORT, check_for_updates, server_preamble
1936
from ephys_link.utils.console import Console
@@ -47,6 +64,9 @@ def __init__(self, options: EphysLinkOptions, platform_handler: PlatformHandler,
4764
# Store connected client.
4865
self._client_sid: str = ""
4966

67+
# Generate Pinpoint ID for proxy usage.
68+
self._pinpoint_id = str(uuid4())[:8]
69+
5070
# Bind events.
5171
self._sio.on("*", self.platform_event_handler)
5272

@@ -63,15 +83,15 @@ def launch(self) -> None:
6383
check_for_updates()
6484

6585
# List platform and available manipulators.
66-
self._console.info_print("PLATFORM", self._platform_handler.get_platform_type())
86+
self._console.info_print("PLATFORM", self._platform_handler.get_display_name())
6787
self._console.info_print(
6888
"MANIPULATORS",
6989
str(get_event_loop().run_until_complete(self._platform_handler.get_manipulators()).manipulators),
7090
)
7191

7292
# Launch server
7393
if self._options.use_proxy:
74-
self._console.info_print("PINPOINT ID", self._platform_handler.get_pinpoint_id().pinpoint_id)
94+
self._console.info_print("PINPOINT ID", self._pinpoint_id)
7595

7696
async def connect_proxy() -> None:
7797
# noinspection HttpUrlsUsage
@@ -204,11 +224,11 @@ async def platform_event_handler(self, event: str, *args: tuple[Any]) -> str:
204224
match event:
205225
# Server metadata.
206226
case "get_version":
207-
return self._platform_handler.get_version()
227+
return __version__
208228
case "get_pinpoint_id":
209-
return str(self._platform_handler.get_pinpoint_id().to_json_string())
210-
case "get_platform_type":
211-
return self._platform_handler.get_platform_type()
229+
return PinpointIdResponse(pinpoint_id=self._pinpoint_id, is_requester=False).to_json_string()
230+
case "get_platform_info":
231+
return (await self._platform_handler.get_platform_info()).to_json_string()
212232

213233
# Manipulator commands.
214234
case "get_manipulators":

src/ephys_link/bindings/fake_binding.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55

66

77
class FakeBinding(BaseBinding):
8-
def __init__(self) -> None:
8+
def __init__(self, *args, **kwargs) -> None:
99
"""Initialize fake manipulator infos."""
1010

11+
super().__init__(*args, **kwargs)
1112
self._positions = [Vector4() for _ in range(8)]
1213
self._angles = [
1314
Vector3(x=90, y=60, z=0),
@@ -20,6 +21,14 @@ def __init__(self) -> None:
2021
Vector3(x=-135, y=30, z=0),
2122
]
2223

24+
@staticmethod
25+
def get_display_name() -> str:
26+
return "Fake Manipulator"
27+
28+
@staticmethod
29+
def get_cli_name() -> str:
30+
return "fake"
31+
2332
async def get_manipulators(self) -> list[str]:
2433
return list(map(str, range(8)))
2534

src/ephys_link/bindings/mpm_binding.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,22 +72,31 @@ class MPMBinding(BaseBinding):
7272
COARSE_SPEED_THRESHOLD = 0.1
7373
INSERTION_SPEED_LIMIT = 9_000
7474

75-
def __init__(self, port: int) -> None:
75+
def __init__(self, port: int = 8080, *args, **kwargs) -> None:
7676
"""Initialize connection to MPM HTTP server.
7777
7878
Args:
7979
port: Port number for MPM HTTP server.
8080
"""
81+
super().__init__(*args, **kwargs)
8182
self._url = f"http://localhost:{port}"
8283
self._movement_stopped = False
8384

85+
@staticmethod
86+
def get_display_name() -> str:
87+
return "Pathfinder MPM Control v2.8.8+"
88+
89+
@staticmethod
90+
def get_cli_name() -> str:
91+
return "pathfinder-mpm"
92+
8493
async def get_manipulators(self) -> list[str]:
8594
return [manipulator["Id"] for manipulator in (await self._query_data())["ProbeArray"]]
8695

8796
async def get_axes_count(self) -> int:
8897
return 3
8998

90-
def get_dimensions(self) -> Vector4:
99+
async def get_dimensions(self) -> Vector4:
91100
return Vector4(x=15, y=15, z=15, w=15)
92101

93102
async def get_position(self, manipulator_id: str) -> Vector4:

0 commit comments

Comments
 (0)