ephys-link 1.3.3__py3-none-any.whl → 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ephys_link/__about__.py +1 -1
- ephys_link/__main__.py +51 -105
- ephys_link/back_end/__init__.py +0 -0
- ephys_link/back_end/platform_handler.py +315 -0
- ephys_link/back_end/server.py +274 -0
- ephys_link/bindings/__init__.py +0 -0
- ephys_link/bindings/fake_binding.py +84 -0
- ephys_link/bindings/mpm_binding.py +315 -0
- ephys_link/bindings/ump_4_binding.py +157 -0
- ephys_link/front_end/__init__.py +0 -0
- ephys_link/front_end/cli.py +104 -0
- ephys_link/front_end/gui.py +204 -0
- ephys_link/utils/__init__.py +0 -0
- ephys_link/utils/base_binding.py +176 -0
- ephys_link/utils/console.py +127 -0
- ephys_link/utils/constants.py +23 -0
- ephys_link/utils/converters.py +86 -0
- ephys_link/utils/startup.py +65 -0
- ephys_link-2.0.0.dist-info/METADATA +91 -0
- ephys_link-2.0.0.dist-info/RECORD +25 -0
- {ephys_link-1.3.3.dist-info → ephys_link-2.0.0.dist-info}/WHEEL +1 -1
- {ephys_link-1.3.3.dist-info → ephys_link-2.0.0.dist-info}/licenses/LICENSE +674 -674
- ephys_link/common.py +0 -49
- ephys_link/emergency_stop.py +0 -67
- ephys_link/gui.py +0 -217
- ephys_link/platform_handler.py +0 -465
- ephys_link/platform_manipulator.py +0 -35
- ephys_link/platforms/__init__.py +0 -5
- ephys_link/platforms/new_scale_handler.py +0 -141
- ephys_link/platforms/new_scale_manipulator.py +0 -312
- ephys_link/platforms/new_scale_pathfinder_handler.py +0 -235
- ephys_link/platforms/sensapex_handler.py +0 -151
- ephys_link/platforms/sensapex_manipulator.py +0 -227
- ephys_link/platforms/ump3_handler.py +0 -57
- ephys_link/platforms/ump3_manipulator.py +0 -147
- ephys_link/resources/CP210xManufacturing.dll +0 -0
- ephys_link/resources/NstMotorCtrl.dll +0 -0
- ephys_link/resources/SiUSBXp.dll +0 -0
- ephys_link/server.py +0 -508
- ephys_link-1.3.3.dist-info/METADATA +0 -164
- ephys_link-1.3.3.dist-info/RECORD +0 -26
- {ephys_link-1.3.3.dist-info → ephys_link-2.0.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Bindings for Sensapex uMp-4 platform.
|
|
2
|
+
|
|
3
|
+
Usage: Instantiate Ump4Bindings to interact with the Sensapex uMp-4 platform.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from asyncio import get_running_loop
|
|
7
|
+
from typing import NoReturn, final, override
|
|
8
|
+
|
|
9
|
+
from sensapex import UMP, SensapexDevice # pyright: ignore [reportMissingTypeStubs]
|
|
10
|
+
from vbl_aquarium.models.unity import Vector4
|
|
11
|
+
|
|
12
|
+
from ephys_link.utils.base_binding import BaseBinding
|
|
13
|
+
from ephys_link.utils.constants import RESOURCES_DIRECTORY
|
|
14
|
+
from ephys_link.utils.converters import (
|
|
15
|
+
list_to_vector4,
|
|
16
|
+
scalar_mm_to_um,
|
|
17
|
+
um_to_mm,
|
|
18
|
+
vector4_to_array,
|
|
19
|
+
vector_mm_to_um,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@final
|
|
24
|
+
class Ump4Binding(BaseBinding):
|
|
25
|
+
"""Bindings for UMP-4 platform"""
|
|
26
|
+
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
"""Initialize UMP-4 bindings."""
|
|
29
|
+
|
|
30
|
+
# Establish connection to Sensapex API (exit if connection fails).
|
|
31
|
+
UMP.set_library_path(RESOURCES_DIRECTORY)
|
|
32
|
+
self._ump = UMP.get_ump() # pyright: ignore [reportUnknownMemberType]
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
@override
|
|
36
|
+
def get_display_name() -> str:
|
|
37
|
+
return "Sensapex uMp-4"
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
@override
|
|
41
|
+
def get_cli_name() -> str:
|
|
42
|
+
return "ump-4"
|
|
43
|
+
|
|
44
|
+
@override
|
|
45
|
+
async def get_manipulators(self) -> list[str]:
|
|
46
|
+
return list(map(str, self._ump.list_devices()))
|
|
47
|
+
|
|
48
|
+
@override
|
|
49
|
+
async def get_axes_count(self) -> int:
|
|
50
|
+
return 4
|
|
51
|
+
|
|
52
|
+
@override
|
|
53
|
+
def get_dimensions(self) -> Vector4:
|
|
54
|
+
return Vector4(x=20, y=20, z=20, w=20)
|
|
55
|
+
|
|
56
|
+
@override
|
|
57
|
+
async def get_position(self, manipulator_id: str) -> Vector4:
|
|
58
|
+
return um_to_mm(list_to_vector4(self._get_device(manipulator_id).get_pos(1))) # pyright: ignore [reportUnknownMemberType]
|
|
59
|
+
|
|
60
|
+
@override
|
|
61
|
+
async def get_angles(self, manipulator_id: str) -> NoReturn:
|
|
62
|
+
"""uMp-4 does not support getting angles so raise an error.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
AttributeError: uMp-4 does not support getting angles.
|
|
66
|
+
"""
|
|
67
|
+
error_message = "UMP-4 does not support getting angles"
|
|
68
|
+
raise AttributeError(error_message)
|
|
69
|
+
|
|
70
|
+
@override
|
|
71
|
+
async def get_shank_count(self, manipulator_id: str) -> NoReturn:
|
|
72
|
+
"""uMp-4 does not support getting shank count so raise an error.
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
AttributeError: uMp-4 does not support getting shank count.
|
|
76
|
+
"""
|
|
77
|
+
error_message = "UMP-4 does not support getting shank count"
|
|
78
|
+
raise AttributeError(error_message)
|
|
79
|
+
|
|
80
|
+
@override
|
|
81
|
+
def get_movement_tolerance(self) -> float:
|
|
82
|
+
return 0.001
|
|
83
|
+
|
|
84
|
+
@override
|
|
85
|
+
async def set_position(self, manipulator_id: str, position: Vector4, speed: float) -> Vector4:
|
|
86
|
+
# Convert position to micrometers.
|
|
87
|
+
target_position_um = vector_mm_to_um(position)
|
|
88
|
+
|
|
89
|
+
# Request movement.
|
|
90
|
+
movement = self._get_device(manipulator_id).goto_pos( # pyright: ignore [reportUnknownMemberType]
|
|
91
|
+
vector4_to_array(target_position_um), scalar_mm_to_um(speed)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Wait for movement to finish.
|
|
95
|
+
_ = await get_running_loop().run_in_executor(None, movement.finished_event.wait, None)
|
|
96
|
+
|
|
97
|
+
# Handle interrupted movement.
|
|
98
|
+
if movement.interrupted:
|
|
99
|
+
error_message = f"Manipulator {manipulator_id} interrupted: {movement.interrupt_reason}" # pyright: ignore [reportUnknownMemberType]
|
|
100
|
+
raise RuntimeError(error_message)
|
|
101
|
+
|
|
102
|
+
# Handle empty end position.
|
|
103
|
+
if movement.last_pos is None or len(movement.last_pos) == 0: # pyright: ignore [reportUnknownMemberType, reportUnknownArgumentType]
|
|
104
|
+
error_message = f"Manipulator {manipulator_id} did not reach target position"
|
|
105
|
+
raise RuntimeError(error_message)
|
|
106
|
+
|
|
107
|
+
return um_to_mm(list_to_vector4(movement.last_pos)) # pyright: ignore [reportArgumentType, reportUnknownMemberType]
|
|
108
|
+
|
|
109
|
+
@override
|
|
110
|
+
async def set_depth(self, manipulator_id: str, depth: float, speed: float) -> float:
|
|
111
|
+
# Augment current position with depth.
|
|
112
|
+
current_position = await self.get_position(manipulator_id)
|
|
113
|
+
new_platform_position = current_position.model_copy(update={"w": depth})
|
|
114
|
+
|
|
115
|
+
# Make the movement.
|
|
116
|
+
final_platform_position = await self.set_position(manipulator_id, new_platform_position, speed)
|
|
117
|
+
|
|
118
|
+
# Return the final depth.
|
|
119
|
+
return float(final_platform_position.w)
|
|
120
|
+
|
|
121
|
+
@override
|
|
122
|
+
async def stop(self, manipulator_id: str) -> None:
|
|
123
|
+
self._get_device(manipulator_id).stop()
|
|
124
|
+
|
|
125
|
+
@override
|
|
126
|
+
def platform_space_to_unified_space(self, platform_space: Vector4) -> Vector4:
|
|
127
|
+
# unified <- platform
|
|
128
|
+
# +x <- +y
|
|
129
|
+
# +y <- -z
|
|
130
|
+
# +z <- +x
|
|
131
|
+
# +d <- +d
|
|
132
|
+
|
|
133
|
+
return Vector4(
|
|
134
|
+
x=platform_space.y,
|
|
135
|
+
y=self.get_dimensions().z - platform_space.z,
|
|
136
|
+
z=platform_space.x,
|
|
137
|
+
w=platform_space.w,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
@override
|
|
141
|
+
def unified_space_to_platform_space(self, unified_space: Vector4) -> Vector4:
|
|
142
|
+
# platform <- unified
|
|
143
|
+
# +x <- +z
|
|
144
|
+
# +y <- +x
|
|
145
|
+
# +z <- -y
|
|
146
|
+
# +d <- +d
|
|
147
|
+
|
|
148
|
+
return Vector4(
|
|
149
|
+
x=unified_space.z,
|
|
150
|
+
y=unified_space.x,
|
|
151
|
+
z=self.get_dimensions().z - unified_space.y,
|
|
152
|
+
w=unified_space.w,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Helper methods.
|
|
156
|
+
def _get_device(self, manipulator_id: str) -> SensapexDevice:
|
|
157
|
+
return self._ump.get_device(int(manipulator_id)) # pyright: ignore [reportUnknownMemberType]
|
|
File without changes
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Command-line interface for the Electrophysiology Manipulator Link.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
Instantiate CLI and call `parse_args()` to get the parsed arguments.
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
CLI().parse_args()
|
|
8
|
+
```
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from argparse import ArgumentParser
|
|
12
|
+
from typing import final
|
|
13
|
+
|
|
14
|
+
from vbl_aquarium.models.ephys_link import EphysLinkOptions
|
|
15
|
+
|
|
16
|
+
from ephys_link.__about__ import __version__ as version
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@final
|
|
20
|
+
class CLI:
|
|
21
|
+
"""Command-line interface for the Electrophysiology Manipulator Link.
|
|
22
|
+
|
|
23
|
+
Configures the CLI parser and options.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self) -> None:
|
|
27
|
+
"""Initialize CLI parser."""
|
|
28
|
+
|
|
29
|
+
self._parser = ArgumentParser(
|
|
30
|
+
description="Electrophysiology Manipulator Link: a Socket.IO interface for manipulators in electrophysiology experiments.",
|
|
31
|
+
prog="python -m ephys-link",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
_ = self._parser.add_argument(
|
|
35
|
+
"-b", "--background", dest="background", action="store_true", help="Skip configuration window."
|
|
36
|
+
)
|
|
37
|
+
_ = self._parser.add_argument(
|
|
38
|
+
"-i",
|
|
39
|
+
"--ignore-updates",
|
|
40
|
+
dest="ignore_updates",
|
|
41
|
+
action="store_true",
|
|
42
|
+
help="Skip (ignore) checking for updates.",
|
|
43
|
+
)
|
|
44
|
+
_ = self._parser.add_argument(
|
|
45
|
+
"-t",
|
|
46
|
+
"--type",
|
|
47
|
+
type=str,
|
|
48
|
+
dest="type",
|
|
49
|
+
default="ump-4",
|
|
50
|
+
help='Manipulator type (i.e. "ump-4", "pathfinder-mpm", "fake"). Default: "ump-4".',
|
|
51
|
+
)
|
|
52
|
+
_ = self._parser.add_argument(
|
|
53
|
+
"-d",
|
|
54
|
+
"--debug",
|
|
55
|
+
dest="debug",
|
|
56
|
+
action="store_true",
|
|
57
|
+
help="Enable debug mode.",
|
|
58
|
+
)
|
|
59
|
+
_ = self._parser.add_argument(
|
|
60
|
+
"-p",
|
|
61
|
+
"--use-proxy",
|
|
62
|
+
dest="use_proxy",
|
|
63
|
+
action="store_true",
|
|
64
|
+
help="Enable proxy mode.",
|
|
65
|
+
)
|
|
66
|
+
_ = self._parser.add_argument(
|
|
67
|
+
"-a",
|
|
68
|
+
"--proxy-address",
|
|
69
|
+
type=str,
|
|
70
|
+
default="proxy2.virtualbrainlab.org",
|
|
71
|
+
dest="proxy_address",
|
|
72
|
+
help="Proxy IP address.",
|
|
73
|
+
)
|
|
74
|
+
_ = self._parser.add_argument(
|
|
75
|
+
"--mpm-port",
|
|
76
|
+
type=int,
|
|
77
|
+
default=8080,
|
|
78
|
+
dest="mpm_port",
|
|
79
|
+
help="Port New Scale Pathfinder MPM's server is on. Default: 8080.",
|
|
80
|
+
)
|
|
81
|
+
_ = self._parser.add_argument(
|
|
82
|
+
"-s",
|
|
83
|
+
"--serial",
|
|
84
|
+
type=str,
|
|
85
|
+
default="no-e-stop",
|
|
86
|
+
dest="serial",
|
|
87
|
+
nargs="?",
|
|
88
|
+
help="Emergency stop serial port (i.e. COM3). Default: disables emergency stop.",
|
|
89
|
+
)
|
|
90
|
+
_ = self._parser.add_argument(
|
|
91
|
+
"-v",
|
|
92
|
+
"--version",
|
|
93
|
+
action="version",
|
|
94
|
+
version=f"Electrophysiology Manipulator Link v{version}",
|
|
95
|
+
help="Print version and exit.",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def parse_args(self) -> EphysLinkOptions:
|
|
99
|
+
"""Parse arguments and return them
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Parsed arguments
|
|
103
|
+
"""
|
|
104
|
+
return EphysLinkOptions(**vars(self._parser.parse_args())) # pyright: ignore [reportAny]
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Graphical User Interface for Ephys Link.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
Create a GUI instance and call `get_options()` to get the options.
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
GUI().get_options()
|
|
8
|
+
```
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from json import load
|
|
12
|
+
from os import makedirs
|
|
13
|
+
from os.path import exists, join
|
|
14
|
+
from socket import gethostbyname, gethostname
|
|
15
|
+
from sys import exit
|
|
16
|
+
from tkinter import CENTER, RIGHT, BooleanVar, E, IntVar, StringVar, Tk, ttk
|
|
17
|
+
from typing import final
|
|
18
|
+
|
|
19
|
+
from platformdirs import user_config_dir
|
|
20
|
+
from vbl_aquarium.models.ephys_link import EphysLinkOptions
|
|
21
|
+
|
|
22
|
+
from ephys_link.__about__ import __version__ as version
|
|
23
|
+
from ephys_link.utils.startup import get_binding_display_to_cli_name
|
|
24
|
+
|
|
25
|
+
# Define options path.
|
|
26
|
+
OPTIONS_DIR = join(user_config_dir(), "VBL", "Ephys Link")
|
|
27
|
+
OPTIONS_FILENAME = "options.json"
|
|
28
|
+
OPTIONS_PATH = join(OPTIONS_DIR, OPTIONS_FILENAME)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@final
|
|
32
|
+
class GUI:
|
|
33
|
+
"""Graphical User Interface for Ephys Link.
|
|
34
|
+
|
|
35
|
+
Gathers options from the user and saves them to a file.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self) -> None:
|
|
39
|
+
"""Setup GUI properties."""
|
|
40
|
+
|
|
41
|
+
self._root = Tk()
|
|
42
|
+
|
|
43
|
+
# Create default options.
|
|
44
|
+
options = EphysLinkOptions()
|
|
45
|
+
|
|
46
|
+
# Read options.
|
|
47
|
+
if exists(OPTIONS_PATH):
|
|
48
|
+
with open(OPTIONS_PATH) as options_file:
|
|
49
|
+
options = EphysLinkOptions(**load(options_file)) # pyright: ignore [reportAny]
|
|
50
|
+
|
|
51
|
+
# Load options into GUI variables.
|
|
52
|
+
self._ignore_updates = BooleanVar(value=options.ignore_updates)
|
|
53
|
+
self._type = StringVar(value=options.type)
|
|
54
|
+
self._debug = BooleanVar(value=options.debug)
|
|
55
|
+
self._use_proxy = BooleanVar(value=options.use_proxy)
|
|
56
|
+
self._proxy_address = StringVar(value=options.proxy_address)
|
|
57
|
+
self._mpm_port = IntVar(value=options.mpm_port)
|
|
58
|
+
self._serial = StringVar(value=options.serial)
|
|
59
|
+
|
|
60
|
+
# Submit flag.
|
|
61
|
+
self._submit = False
|
|
62
|
+
|
|
63
|
+
def get_options(self) -> EphysLinkOptions:
|
|
64
|
+
"""Get options from GUI.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Options gathered from the GUI.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
# Launch GUI.
|
|
71
|
+
self._build_gui()
|
|
72
|
+
self._root.mainloop()
|
|
73
|
+
|
|
74
|
+
# Exit if the user did not submit options.
|
|
75
|
+
if not self._submit:
|
|
76
|
+
exit(1)
|
|
77
|
+
|
|
78
|
+
# Extract options from GUI.
|
|
79
|
+
options = EphysLinkOptions(
|
|
80
|
+
ignore_updates=self._ignore_updates.get(),
|
|
81
|
+
type=self._type.get(),
|
|
82
|
+
debug=self._debug.get(),
|
|
83
|
+
use_proxy=self._use_proxy.get(),
|
|
84
|
+
proxy_address=self._proxy_address.get(),
|
|
85
|
+
mpm_port=self._mpm_port.get(),
|
|
86
|
+
serial=self._serial.get(),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Save options.
|
|
90
|
+
makedirs(OPTIONS_DIR, exist_ok=True)
|
|
91
|
+
with open(OPTIONS_PATH, "w+") as options_file:
|
|
92
|
+
_ = options_file.write(options.model_dump_json())
|
|
93
|
+
|
|
94
|
+
# Return options
|
|
95
|
+
return options
|
|
96
|
+
|
|
97
|
+
def _build_gui(self) -> None:
|
|
98
|
+
"""Build GUI."""
|
|
99
|
+
|
|
100
|
+
self._root.title(f"Ephys Link v{version}")
|
|
101
|
+
|
|
102
|
+
mainframe = ttk.Frame(self._root, padding=3)
|
|
103
|
+
mainframe.grid(column=0, row=0, sticky="news")
|
|
104
|
+
_ = self._root.columnconfigure(0, weight=1)
|
|
105
|
+
_ = self._root.rowconfigure(0, weight=1)
|
|
106
|
+
_ = mainframe.columnconfigure(0, weight=1)
|
|
107
|
+
_ = mainframe.rowconfigure(0, weight=1)
|
|
108
|
+
|
|
109
|
+
# Server serving settings.
|
|
110
|
+
|
|
111
|
+
server_serving_settings = ttk.LabelFrame(mainframe, text="Serving Settings", padding=3)
|
|
112
|
+
server_serving_settings.grid(column=0, row=0, sticky="news")
|
|
113
|
+
|
|
114
|
+
# Local IP.
|
|
115
|
+
ttk.Label(server_serving_settings, text="Local IP:", anchor=E, justify=RIGHT).grid(column=0, row=0, sticky="we")
|
|
116
|
+
ttk.Label(server_serving_settings, text=gethostbyname(gethostname())).grid(column=1, row=0, sticky="we")
|
|
117
|
+
|
|
118
|
+
# Proxy.
|
|
119
|
+
ttk.Label(server_serving_settings, text="Use Proxy:", anchor=E, justify=RIGHT).grid(
|
|
120
|
+
column=0, row=1, sticky="we"
|
|
121
|
+
)
|
|
122
|
+
ttk.Checkbutton(
|
|
123
|
+
server_serving_settings,
|
|
124
|
+
variable=self._use_proxy,
|
|
125
|
+
).grid(column=1, row=1, sticky="we")
|
|
126
|
+
|
|
127
|
+
# Proxy address.
|
|
128
|
+
ttk.Label(server_serving_settings, text="Proxy Address:", anchor=E, justify=RIGHT).grid(
|
|
129
|
+
column=0, row=2, sticky="we"
|
|
130
|
+
)
|
|
131
|
+
ttk.Entry(server_serving_settings, textvariable=self._proxy_address, justify=CENTER).grid(
|
|
132
|
+
column=1, row=2, sticky="we"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Ignore updates.
|
|
136
|
+
ttk.Label(server_serving_settings, text="Ignore Updates:", anchor=E, justify=RIGHT).grid(
|
|
137
|
+
column=0, row=4, sticky="we"
|
|
138
|
+
)
|
|
139
|
+
ttk.Checkbutton(
|
|
140
|
+
server_serving_settings,
|
|
141
|
+
variable=self._ignore_updates,
|
|
142
|
+
).grid(column=1, row=4, sticky="we")
|
|
143
|
+
|
|
144
|
+
# Debug mode.
|
|
145
|
+
ttk.Label(server_serving_settings, text="Debug mode:", anchor=E, justify=RIGHT).grid(
|
|
146
|
+
column=0, row=5, sticky="we"
|
|
147
|
+
)
|
|
148
|
+
ttk.Checkbutton(
|
|
149
|
+
server_serving_settings,
|
|
150
|
+
variable=self._debug,
|
|
151
|
+
).grid(column=1, row=5, sticky="we")
|
|
152
|
+
|
|
153
|
+
# ---
|
|
154
|
+
|
|
155
|
+
# Platform type.
|
|
156
|
+
platform_type_settings = ttk.LabelFrame(mainframe, text="Platform Type", padding=3)
|
|
157
|
+
platform_type_settings.grid(column=0, row=1, sticky="news")
|
|
158
|
+
|
|
159
|
+
for index, (display_name, cli_name) in enumerate(get_binding_display_to_cli_name().items()):
|
|
160
|
+
ttk.Radiobutton(
|
|
161
|
+
platform_type_settings,
|
|
162
|
+
text=display_name,
|
|
163
|
+
variable=self._type,
|
|
164
|
+
value=cli_name,
|
|
165
|
+
).grid(column=0, row=index, sticky="we")
|
|
166
|
+
|
|
167
|
+
# ---
|
|
168
|
+
|
|
169
|
+
# New Scale Settings.
|
|
170
|
+
new_scale_settings = ttk.LabelFrame(mainframe, text="Pathfinder MPM Settings", padding=3)
|
|
171
|
+
new_scale_settings.grid(column=0, row=2, sticky="news")
|
|
172
|
+
|
|
173
|
+
# Port
|
|
174
|
+
ttk.Label(new_scale_settings, text="HTTP Server Port:", anchor=E, justify=RIGHT).grid(
|
|
175
|
+
column=0, row=1, sticky="we"
|
|
176
|
+
)
|
|
177
|
+
ttk.Entry(new_scale_settings, textvariable=self._mpm_port, width=5, justify=CENTER).grid(
|
|
178
|
+
column=1, row=1, sticky="we"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# ---
|
|
182
|
+
|
|
183
|
+
# Emergency Stop serial port.
|
|
184
|
+
e_stop_settings = ttk.LabelFrame(mainframe, text="Emergency Stop Settings", padding=3)
|
|
185
|
+
e_stop_settings.grid(column=0, row=3, sticky="news")
|
|
186
|
+
|
|
187
|
+
# Serial Port
|
|
188
|
+
ttk.Label(e_stop_settings, text="Serial Port:", anchor=E, justify=RIGHT).grid(column=0, row=1, sticky="we")
|
|
189
|
+
ttk.Entry(e_stop_settings, textvariable=self._serial, justify=CENTER).grid(column=1, row=1, sticky="we")
|
|
190
|
+
|
|
191
|
+
# Server launch button.
|
|
192
|
+
ttk.Button(
|
|
193
|
+
mainframe,
|
|
194
|
+
text="Launch Server",
|
|
195
|
+
command=self._launch_server,
|
|
196
|
+
).grid(column=0, row=4, columnspan=2, sticky="we")
|
|
197
|
+
|
|
198
|
+
def _launch_server(self) -> None:
|
|
199
|
+
"""Close GUI and return to the server.
|
|
200
|
+
|
|
201
|
+
Options are saved in fields.
|
|
202
|
+
"""
|
|
203
|
+
self._submit = True
|
|
204
|
+
self._root.destroy()
|
|
File without changes
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Binding methods for Ephys Link manipulator platforms.
|
|
2
|
+
|
|
3
|
+
Definition of the methods a platform binding class must implement to be used by Ephys Link.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
Implement the BaseBindings class when defining a platform binding to ensure it supports the necessary methods.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
|
|
11
|
+
from vbl_aquarium.models.unity import Vector3, Vector4
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaseBinding(ABC):
|
|
15
|
+
"""Base class to enforce bindings manipulator platforms will support.
|
|
16
|
+
|
|
17
|
+
No need to catch exceptions as the [Platform Handler][ephys_link.back_end.platform_handler] will catch them.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def get_display_name() -> str:
|
|
23
|
+
"""Get the full display name of the platform.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Full display name of the platform.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def get_cli_name() -> str:
|
|
32
|
+
"""Get the name of the platform for CLI usage.
|
|
33
|
+
|
|
34
|
+
This is the value used to identify the platform when using the `-t` flag in the CLI.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Name of the platform to use on the CLI.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
async def get_axes_count(self) -> int:
|
|
42
|
+
"""Get the number of axes for the current platform.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Number of axes.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def get_dimensions(self) -> Vector4:
|
|
50
|
+
"""Get the dimensions of the manipulators on the current platform (mm).
|
|
51
|
+
|
|
52
|
+
For 3-axis manipulators, copy the dimension of the axis parallel to the probe into w.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Dimensions of the manipulators.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
async def get_manipulators(self) -> list[str]:
|
|
60
|
+
"""Get a list of available manipulators on the current platform.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
List of manipulator IDs.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
@abstractmethod
|
|
67
|
+
async def get_position(self, manipulator_id: str) -> Vector4:
|
|
68
|
+
"""Get the current position of a manipulator.
|
|
69
|
+
|
|
70
|
+
These will be the translation values of the manipulator (mm), so they may need to be rotated to unified space.
|
|
71
|
+
For 3-axis manipulators, copy the position of the axis parallel to the probe into w.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
manipulator_id: Manipulator ID.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Current position of the manipulator in platform space (mm).
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
@abstractmethod
|
|
81
|
+
async def get_angles(self, manipulator_id: str) -> Vector3:
|
|
82
|
+
"""Get the current rotation angles of a manipulator in Yaw, Pitch, Roll (degrees).
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
manipulator_id: Manipulator ID.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Current angles of the manipulator.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
@abstractmethod
|
|
92
|
+
async def get_shank_count(self, manipulator_id: str) -> int:
|
|
93
|
+
"""Get the number of shanks on a manipulator.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
manipulator_id: Manipulator ID.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Number of shanks on the manipulator.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
@abstractmethod
|
|
103
|
+
def get_movement_tolerance(self) -> float:
|
|
104
|
+
"""Get the tolerance for how close the final position must be to the target position in a movement (mm).
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Movement tolerance (mm).
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
@abstractmethod
|
|
111
|
+
async def set_position(self, manipulator_id: str, position: Vector4, speed: float) -> Vector4:
|
|
112
|
+
"""Set the position of a manipulator.
|
|
113
|
+
|
|
114
|
+
This will directly set the position in the original platform space.
|
|
115
|
+
For 3-axis manipulators, the first 3 values of the position will be used.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
manipulator_id: Manipulator ID.
|
|
119
|
+
position: Platform space position to set the manipulator to (mm).
|
|
120
|
+
speed: Speed to move the manipulator to the position (mm/s).
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Final position of the manipulator in platform space (mm).
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
@abstractmethod
|
|
127
|
+
async def set_depth(self, manipulator_id: str, depth: float, speed: float) -> float:
|
|
128
|
+
"""Set the depth of a manipulator.
|
|
129
|
+
|
|
130
|
+
This will directly set the depth stage in the original platform space.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
manipulator_id: Manipulator ID.
|
|
134
|
+
depth: Depth to set the manipulator to (mm).
|
|
135
|
+
speed: Speed to move the manipulator to the depth (mm/s).
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Final depth of the manipulator in platform space (mm).
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
@abstractmethod
|
|
142
|
+
async def stop(self, manipulator_id: str) -> None:
|
|
143
|
+
"""Stop a manipulator.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
manipulator_id: Manipulator ID.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
@abstractmethod
|
|
150
|
+
def platform_space_to_unified_space(self, platform_space: Vector4) -> Vector4:
|
|
151
|
+
"""Convert platform space coordinates to unified space coordinates.
|
|
152
|
+
|
|
153
|
+
This is an axes-swapping transformation.
|
|
154
|
+
|
|
155
|
+
Unified coordinate space is the standard left-handed cartesian coordinate system
|
|
156
|
+
with an additional depth axis pointing from the base of the probe to the tip.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
platform_space: Platform space coordinates.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Unified space coordinates.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
@abstractmethod
|
|
166
|
+
def unified_space_to_platform_space(self, unified_space: Vector4) -> Vector4:
|
|
167
|
+
"""Convert unified space coordinates to platform space coordinates.
|
|
168
|
+
|
|
169
|
+
This is an axes-swapping transformation.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
unified_space: Unified space coordinates.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Platform space coordinates.
|
|
176
|
+
"""
|