ephys-link 1.3.3__py3-none-any.whl → 2.0.0b1__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 +28 -90
- ephys_link/back_end/__init__.py +0 -0
- ephys_link/back_end/platform_handler.py +298 -0
- ephys_link/back_end/server.py +200 -0
- ephys_link/bindings/__init__.py +0 -0
- ephys_link/bindings/fake_bindings.py +54 -0
- ephys_link/bindings/ump_4_bindings.py +127 -0
- ephys_link/front_end/__init__.py +0 -0
- ephys_link/front_end/cli.py +98 -0
- ephys_link/{gui.py → front_end/gui.py} +93 -95
- ephys_link/util/__init__.py +0 -0
- ephys_link/util/base_bindings.py +133 -0
- ephys_link/util/common.py +121 -0
- ephys_link/util/console.py +112 -0
- {ephys_link-1.3.3.dist-info → ephys_link-2.0.0b1.dist-info}/METADATA +6 -4
- ephys_link-2.0.0b1.dist-info/RECORD +25 -0
- {ephys_link-1.3.3.dist-info → ephys_link-2.0.0b1.dist-info}/WHEEL +1 -1
- ephys_link/common.py +0 -49
- ephys_link/emergency_stop.py +0 -67
- 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/server.py +0 -508
- ephys_link-1.3.3.dist-info/RECORD +0 -26
- {ephys_link-1.3.3.dist-info → ephys_link-2.0.0b1.dist-info}/entry_points.txt +0 -0
- {ephys_link-1.3.3.dist-info → ephys_link-2.0.0b1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from vbl_aquarium.models.unity import Vector3, Vector4
|
|
2
|
+
|
|
3
|
+
from ephys_link.util.base_bindings import BaseBindings
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FakeBindings(BaseBindings):
|
|
7
|
+
def __init__(self) -> None:
|
|
8
|
+
"""Initialize fake manipulator infos."""
|
|
9
|
+
|
|
10
|
+
self._positions = [Vector4() for _ in range(8)]
|
|
11
|
+
self._angles = [
|
|
12
|
+
Vector3(x=90, y=60, z=0),
|
|
13
|
+
Vector3(x=-90, y=60, z=0),
|
|
14
|
+
Vector3(x=180, y=60, z=0),
|
|
15
|
+
Vector3(x=0, y=60, z=0),
|
|
16
|
+
Vector3(x=45, y=30, z=0),
|
|
17
|
+
Vector3(x=-45, y=30, z=0),
|
|
18
|
+
Vector3(x=135, y=30, z=0),
|
|
19
|
+
Vector3(x=-135, y=30, z=0),
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
async def get_manipulators(self) -> list[str]:
|
|
23
|
+
return list(map(str, range(8)))
|
|
24
|
+
|
|
25
|
+
async def get_num_axes(self) -> int:
|
|
26
|
+
return 4
|
|
27
|
+
|
|
28
|
+
def get_dimensions(self) -> Vector4:
|
|
29
|
+
return Vector4(x=20, y=20, z=20, w=20)
|
|
30
|
+
|
|
31
|
+
async def get_position(self, manipulator_id: str) -> Vector4:
|
|
32
|
+
return self._positions[int(manipulator_id)]
|
|
33
|
+
|
|
34
|
+
async def get_angles(self, manipulator_id: str) -> Vector3:
|
|
35
|
+
return self._angles[int(manipulator_id)]
|
|
36
|
+
|
|
37
|
+
async def get_shank_count(self, _: str) -> int:
|
|
38
|
+
return 1
|
|
39
|
+
|
|
40
|
+
async def get_movement_tolerance(self) -> float:
|
|
41
|
+
return 0.001
|
|
42
|
+
|
|
43
|
+
async def set_position(self, manipulator_id: str, position: Vector4, _: float) -> Vector4:
|
|
44
|
+
self._positions[int(manipulator_id)] = position
|
|
45
|
+
return position
|
|
46
|
+
|
|
47
|
+
async def stop(self, _: str) -> None:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
def platform_space_to_unified_space(self, platform_space: Vector4) -> Vector4:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
def unified_space_to_platform_space(self, unified_space: Vector4) -> Vector4:
|
|
54
|
+
pass
|
|
@@ -0,0 +1,127 @@
|
|
|
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
|
+
|
|
8
|
+
from sensapex import UMP, SensapexDevice
|
|
9
|
+
from vbl_aquarium.models.unity import Vector3, Vector4
|
|
10
|
+
|
|
11
|
+
from ephys_link.util.base_bindings import BaseBindings
|
|
12
|
+
from ephys_link.util.common import RESOURCES_PATH, array_to_vector4, mm_to_um, mmps_to_umps, um_to_mm, vector4_to_array
|
|
13
|
+
from ephys_link.util.console import Console
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Ump4Bindings(BaseBindings):
|
|
17
|
+
"""Bindings for UMP-4 platform"""
|
|
18
|
+
|
|
19
|
+
def __init__(self) -> None:
|
|
20
|
+
"""Initialize UMP-4 bindings."""
|
|
21
|
+
|
|
22
|
+
# Establish connection to Sensapex API (exit if connection fails).
|
|
23
|
+
UMP.set_library_path(RESOURCES_PATH)
|
|
24
|
+
self._ump = UMP.get_ump()
|
|
25
|
+
if self._ump is None:
|
|
26
|
+
error_message = "Unable to connect to uMp"
|
|
27
|
+
Console.error_print(error_message)
|
|
28
|
+
raise ValueError(error_message)
|
|
29
|
+
|
|
30
|
+
async def get_manipulators(self) -> list[str]:
|
|
31
|
+
return list(map(str, self._ump.list_devices()))
|
|
32
|
+
|
|
33
|
+
async def get_num_axes(self) -> int:
|
|
34
|
+
return 4
|
|
35
|
+
|
|
36
|
+
def get_dimensions(self) -> Vector4:
|
|
37
|
+
return Vector4(x=20, y=20, z=20, w=20)
|
|
38
|
+
|
|
39
|
+
async def get_position(self, manipulator_id: str) -> Vector4:
|
|
40
|
+
return um_to_mm(array_to_vector4(self._get_device(manipulator_id).get_pos(1)))
|
|
41
|
+
|
|
42
|
+
# noinspection PyTypeChecker
|
|
43
|
+
async def get_angles(self, _: str) -> Vector3:
|
|
44
|
+
"""uMp-4 does not support getting angles so raise an error.
|
|
45
|
+
|
|
46
|
+
:raises: AttributeError
|
|
47
|
+
"""
|
|
48
|
+
error_message = "UMP-4 does not support getting angles"
|
|
49
|
+
raise AttributeError(error_message)
|
|
50
|
+
|
|
51
|
+
# noinspection PyTypeChecker
|
|
52
|
+
async def get_shank_count(self, _: str) -> int:
|
|
53
|
+
"""uMp-4 does not support getting shank count so raise an error.
|
|
54
|
+
|
|
55
|
+
:raises: AttributeError
|
|
56
|
+
"""
|
|
57
|
+
error_message = "UMP-4 does not support getting shank count"
|
|
58
|
+
raise AttributeError(error_message)
|
|
59
|
+
|
|
60
|
+
async def get_movement_tolerance(self) -> float:
|
|
61
|
+
return 0.001
|
|
62
|
+
|
|
63
|
+
async def set_position(self, manipulator_id: str, position: Vector4, speed: float) -> Vector4:
|
|
64
|
+
"""Set the position of the manipulator.
|
|
65
|
+
|
|
66
|
+
Waits using Asyncio until the movement is finished. This assumes the application is running in an event loop.
|
|
67
|
+
|
|
68
|
+
:param manipulator_id: Manipulator ID.
|
|
69
|
+
:type manipulator_id: str
|
|
70
|
+
:param position: Platform space position to set the manipulator to (mm).
|
|
71
|
+
:type position: Vector4
|
|
72
|
+
:param speed: Speed to move the manipulator to the position (mm/s).
|
|
73
|
+
:type speed: float
|
|
74
|
+
:returns: Final position of the manipulator in platform space (mm).
|
|
75
|
+
:rtype: Vector4
|
|
76
|
+
:raises RuntimeError: If the movement is interrupted.
|
|
77
|
+
"""
|
|
78
|
+
# Convert position to micrometers.
|
|
79
|
+
target_position_um = mm_to_um(position)
|
|
80
|
+
|
|
81
|
+
# Request movement.
|
|
82
|
+
movement = self._get_device(manipulator_id).goto_pos(vector4_to_array(target_position_um), mmps_to_umps(speed))
|
|
83
|
+
|
|
84
|
+
# Wait for movement to finish.
|
|
85
|
+
await get_running_loop().run_in_executor(None, movement.finished_event.wait)
|
|
86
|
+
|
|
87
|
+
# Handle interrupted movement.
|
|
88
|
+
if movement.interrupted:
|
|
89
|
+
error_message = f"Manipulator {manipulator_id} interrupted: {movement.interrupt_reason}"
|
|
90
|
+
raise RuntimeError(error_message)
|
|
91
|
+
|
|
92
|
+
return um_to_mm(array_to_vector4(movement.last_pos))
|
|
93
|
+
|
|
94
|
+
async def stop(self, manipulator_id: str) -> None:
|
|
95
|
+
self._get_device(manipulator_id).stop()
|
|
96
|
+
|
|
97
|
+
def platform_space_to_unified_space(self, platform_space: Vector4) -> Vector4:
|
|
98
|
+
# unified <- platform
|
|
99
|
+
# +x <- +y
|
|
100
|
+
# +y <- -z
|
|
101
|
+
# +z <- +x
|
|
102
|
+
# +d <- +d
|
|
103
|
+
|
|
104
|
+
return Vector4(
|
|
105
|
+
x=platform_space.y,
|
|
106
|
+
y=self.get_dimensions().z - platform_space.z,
|
|
107
|
+
z=platform_space.x,
|
|
108
|
+
w=platform_space.w,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def unified_space_to_platform_space(self, unified_space: Vector4) -> Vector4:
|
|
112
|
+
# platform <- unified
|
|
113
|
+
# +x <- +z
|
|
114
|
+
# +y <- +x
|
|
115
|
+
# +z <- -y
|
|
116
|
+
# +d <- +d
|
|
117
|
+
|
|
118
|
+
return Vector4(
|
|
119
|
+
x=unified_space.z,
|
|
120
|
+
y=unified_space.x,
|
|
121
|
+
z=self.get_dimensions().z - unified_space.y,
|
|
122
|
+
w=unified_space.w,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Helper methods.
|
|
126
|
+
def _get_device(self, manipulator_id: str) -> SensapexDevice:
|
|
127
|
+
return self._ump.get_device(int(manipulator_id))
|
|
File without changes
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Command-line interface for the Electrophysiology Manipulator Link.
|
|
2
|
+
|
|
3
|
+
Usage: instantiate CLI and call parse_args() to get the parsed arguments.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from argparse import ArgumentParser
|
|
7
|
+
|
|
8
|
+
from vbl_aquarium.models.ephys_link import EphysLinkOptions
|
|
9
|
+
|
|
10
|
+
from ephys_link.__about__ import __version__ as version
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CLI:
|
|
14
|
+
"""Command-line interface for the Electrophysiology Manipulator Link.
|
|
15
|
+
|
|
16
|
+
Configures the CLI parser and options.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self) -> None:
|
|
20
|
+
"""Initialize CLI parser."""
|
|
21
|
+
|
|
22
|
+
self._parser = ArgumentParser(
|
|
23
|
+
description="Electrophysiology Manipulator Link:"
|
|
24
|
+
" a Socket.IO interface for manipulators in electrophysiology experiments.",
|
|
25
|
+
prog="python -m ephys-link",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
self._parser.add_argument(
|
|
29
|
+
"-b", "--background", dest="background", action="store_true", help="Skip configuration window."
|
|
30
|
+
)
|
|
31
|
+
self._parser.add_argument(
|
|
32
|
+
"-i",
|
|
33
|
+
"--ignore-updates",
|
|
34
|
+
dest="ignore_updates",
|
|
35
|
+
action="store_true",
|
|
36
|
+
help="Skip (ignore) checking for updates.",
|
|
37
|
+
)
|
|
38
|
+
self._parser.add_argument(
|
|
39
|
+
"-t",
|
|
40
|
+
"--type",
|
|
41
|
+
type=str,
|
|
42
|
+
dest="type",
|
|
43
|
+
default="ump-4",
|
|
44
|
+
help='Manipulator type (i.e. "ump-4", "ump-3", "pathfinder-mpm", "new-scale", "fake"). Default: "ump-4".',
|
|
45
|
+
)
|
|
46
|
+
self._parser.add_argument(
|
|
47
|
+
"-d",
|
|
48
|
+
"--debug",
|
|
49
|
+
dest="debug",
|
|
50
|
+
action="store_true",
|
|
51
|
+
help="Enable debug mode.",
|
|
52
|
+
)
|
|
53
|
+
self._parser.add_argument(
|
|
54
|
+
"-p",
|
|
55
|
+
"--use-proxy",
|
|
56
|
+
dest="use_proxy",
|
|
57
|
+
action="store_true",
|
|
58
|
+
help="Enable proxy mode.",
|
|
59
|
+
)
|
|
60
|
+
self._parser.add_argument(
|
|
61
|
+
"-a",
|
|
62
|
+
"--proxy-address",
|
|
63
|
+
type=str,
|
|
64
|
+
default="proxy2.virtualbrainlab.org",
|
|
65
|
+
dest="proxy_address",
|
|
66
|
+
help="Proxy IP address.",
|
|
67
|
+
)
|
|
68
|
+
self._parser.add_argument(
|
|
69
|
+
"--mpm-port",
|
|
70
|
+
type=int,
|
|
71
|
+
default=8080,
|
|
72
|
+
dest="mpm_port",
|
|
73
|
+
help="Port New Scale Pathfinder MPM's server is on. Default: 8080.",
|
|
74
|
+
)
|
|
75
|
+
self._parser.add_argument(
|
|
76
|
+
"-s",
|
|
77
|
+
"--serial",
|
|
78
|
+
type=str,
|
|
79
|
+
default="no-e-stop",
|
|
80
|
+
dest="serial",
|
|
81
|
+
nargs="?",
|
|
82
|
+
help="Emergency stop serial port (i.e. COM3). Default: disables emergency stop.",
|
|
83
|
+
)
|
|
84
|
+
self._parser.add_argument(
|
|
85
|
+
"-v",
|
|
86
|
+
"--version",
|
|
87
|
+
action="version",
|
|
88
|
+
version=f"Electrophysiology Manipulator Link v{version}",
|
|
89
|
+
help="Print version and exit.",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def parse_args(self) -> EphysLinkOptions:
|
|
93
|
+
"""Parse arguments and return them
|
|
94
|
+
|
|
95
|
+
:returns: Parsed arguments
|
|
96
|
+
:rtype: EphysLinkOptions
|
|
97
|
+
"""
|
|
98
|
+
return EphysLinkOptions(**vars(self._parser.parse_args()))
|
|
@@ -1,64 +1,89 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
"""Graphical User Interface for Ephys Link.
|
|
2
|
+
|
|
3
|
+
Usage: create a GUI instance and call get_options() to get the options.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from json import load
|
|
3
7
|
from os import makedirs
|
|
4
|
-
from os.path import exists
|
|
8
|
+
from os.path import exists, join
|
|
5
9
|
from socket import gethostbyname, gethostname
|
|
10
|
+
from sys import exit
|
|
6
11
|
from tkinter import CENTER, RIGHT, BooleanVar, E, IntVar, StringVar, Tk, ttk
|
|
7
12
|
|
|
8
13
|
from platformdirs import user_config_dir
|
|
14
|
+
from vbl_aquarium.models.ephys_link import EphysLinkOptions
|
|
9
15
|
|
|
10
|
-
import ephys_link.common as com
|
|
11
16
|
from ephys_link.__about__ import __version__ as version
|
|
12
|
-
from ephys_link.emergency_stop import EmergencyStop
|
|
13
|
-
from ephys_link.server import Server
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
# Define options path.
|
|
19
|
+
OPTIONS_DIR = join(user_config_dir(), "VBL", "Ephys Link")
|
|
20
|
+
OPTIONS_FILENAME = "options.json"
|
|
21
|
+
OPTIONS_PATH = join(OPTIONS_DIR, OPTIONS_FILENAME)
|
|
17
22
|
|
|
18
23
|
|
|
19
24
|
class GUI:
|
|
20
|
-
"""
|
|
25
|
+
"""Graphical User Interface for Ephys Link.
|
|
26
|
+
|
|
27
|
+
Gathers options from the user and saves them to a file.
|
|
28
|
+
"""
|
|
21
29
|
|
|
22
30
|
def __init__(self) -> None:
|
|
23
|
-
"""Setup
|
|
31
|
+
"""Setup GUI properties."""
|
|
24
32
|
|
|
25
33
|
self._root = Tk()
|
|
26
34
|
|
|
27
|
-
# Create default
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
self.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
self._serial = StringVar(value=settings["serial"])
|
|
52
|
-
|
|
53
|
-
def launch(self) -> None:
|
|
54
|
-
"""Build and launch GUI"""
|
|
55
|
-
|
|
56
|
-
# Build and run GUI.
|
|
35
|
+
# Create default options.
|
|
36
|
+
options = EphysLinkOptions()
|
|
37
|
+
|
|
38
|
+
# Read options.
|
|
39
|
+
if exists(OPTIONS_PATH):
|
|
40
|
+
with open(OPTIONS_PATH) as options_file:
|
|
41
|
+
options = EphysLinkOptions(**load(options_file))
|
|
42
|
+
|
|
43
|
+
# Load options into GUI variables.
|
|
44
|
+
self._ignore_updates = BooleanVar(value=options.ignore_updates)
|
|
45
|
+
self._type = StringVar(value=options.type)
|
|
46
|
+
self._debug = BooleanVar(value=options.debug)
|
|
47
|
+
self._use_proxy = BooleanVar(value=options.use_proxy)
|
|
48
|
+
self._proxy_address = StringVar(value=options.proxy_address)
|
|
49
|
+
self._mpm_port = IntVar(value=options.mpm_port)
|
|
50
|
+
self._serial = StringVar(value=options.serial)
|
|
51
|
+
|
|
52
|
+
# Submit flag.
|
|
53
|
+
self._submit = False
|
|
54
|
+
|
|
55
|
+
def get_options(self) -> EphysLinkOptions:
|
|
56
|
+
"""Get options from GUI."""
|
|
57
|
+
|
|
58
|
+
# Launch GUI.
|
|
57
59
|
self._build_gui()
|
|
58
60
|
self._root.mainloop()
|
|
59
61
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
# Exit if the user did not submit options.
|
|
63
|
+
if not self._submit:
|
|
64
|
+
exit(1)
|
|
65
|
+
|
|
66
|
+
# Extract options from GUI.
|
|
67
|
+
options = EphysLinkOptions(
|
|
68
|
+
ignore_updates=self._ignore_updates.get(),
|
|
69
|
+
type=self._type.get(),
|
|
70
|
+
debug=self._debug.get(),
|
|
71
|
+
use_proxy=self._use_proxy.get(),
|
|
72
|
+
proxy_address=self._proxy_address.get(),
|
|
73
|
+
mpm_port=self._mpm_port.get(),
|
|
74
|
+
serial=self._serial.get(),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Save options.
|
|
78
|
+
makedirs(OPTIONS_DIR, exist_ok=True)
|
|
79
|
+
with open(OPTIONS_PATH, "w+") as options_file:
|
|
80
|
+
options_file.write(options.model_dump_json())
|
|
81
|
+
|
|
82
|
+
# Return options
|
|
83
|
+
return options
|
|
84
|
+
|
|
85
|
+
def _build_gui(self) -> None:
|
|
86
|
+
"""Build GUI."""
|
|
62
87
|
|
|
63
88
|
self._root.title(f"Ephys Link v{version}")
|
|
64
89
|
|
|
@@ -84,7 +109,7 @@ class GUI:
|
|
|
84
109
|
)
|
|
85
110
|
ttk.Checkbutton(
|
|
86
111
|
server_serving_settings,
|
|
87
|
-
variable=self.
|
|
112
|
+
variable=self._use_proxy,
|
|
88
113
|
).grid(column=1, row=1, sticky="we")
|
|
89
114
|
|
|
90
115
|
# Proxy address.
|
|
@@ -95,12 +120,6 @@ class GUI:
|
|
|
95
120
|
column=1, row=2, sticky="we"
|
|
96
121
|
)
|
|
97
122
|
|
|
98
|
-
# Port.
|
|
99
|
-
ttk.Label(server_serving_settings, text="Port:", anchor=E, justify=RIGHT).grid(column=0, row=3, sticky="we")
|
|
100
|
-
ttk.Entry(server_serving_settings, textvariable=self._port, width=5, justify=CENTER).grid(
|
|
101
|
-
column=1, row=3, sticky="we"
|
|
102
|
-
)
|
|
103
|
-
|
|
104
123
|
# Ignore updates.
|
|
105
124
|
ttk.Label(server_serving_settings, text="Ignore Updates:", anchor=E, justify=RIGHT).grid(
|
|
106
125
|
column=0, row=4, sticky="we"
|
|
@@ -110,6 +129,15 @@ class GUI:
|
|
|
110
129
|
variable=self._ignore_updates,
|
|
111
130
|
).grid(column=1, row=4, sticky="we")
|
|
112
131
|
|
|
132
|
+
# Debug mode.
|
|
133
|
+
ttk.Label(server_serving_settings, text="Debug mode:", anchor=E, justify=RIGHT).grid(
|
|
134
|
+
column=0, row=5, sticky="we"
|
|
135
|
+
)
|
|
136
|
+
ttk.Checkbutton(
|
|
137
|
+
server_serving_settings,
|
|
138
|
+
variable=self._debug,
|
|
139
|
+
).grid(column=1, row=5, sticky="we")
|
|
140
|
+
|
|
113
141
|
# ---
|
|
114
142
|
|
|
115
143
|
# Platform type.
|
|
@@ -120,38 +148,44 @@ class GUI:
|
|
|
120
148
|
platform_type_settings,
|
|
121
149
|
text="Sensapex uMp-4",
|
|
122
150
|
variable=self._type,
|
|
123
|
-
value="
|
|
151
|
+
value="ump-4",
|
|
124
152
|
).grid(column=0, row=0, sticky="we")
|
|
125
153
|
ttk.Radiobutton(
|
|
126
154
|
platform_type_settings,
|
|
127
155
|
text="Sensapex uMp-3",
|
|
128
156
|
variable=self._type,
|
|
129
|
-
value="
|
|
157
|
+
value="ump-3",
|
|
130
158
|
).grid(column=0, row=1, sticky="we")
|
|
131
159
|
ttk.Radiobutton(
|
|
132
160
|
platform_type_settings,
|
|
133
161
|
text="Pathfinder MPM Control v2.8.8+",
|
|
134
162
|
variable=self._type,
|
|
135
|
-
value="
|
|
163
|
+
value="pathfinder-mpm",
|
|
136
164
|
).grid(column=0, row=2, sticky="we")
|
|
137
165
|
ttk.Radiobutton(
|
|
138
166
|
platform_type_settings,
|
|
139
167
|
text="New Scale M3-USB-3:1-EP",
|
|
140
168
|
variable=self._type,
|
|
141
|
-
value="
|
|
169
|
+
value="new-scale",
|
|
142
170
|
).grid(column=0, row=3, sticky="we")
|
|
171
|
+
ttk.Radiobutton(
|
|
172
|
+
platform_type_settings,
|
|
173
|
+
text="Fake Platform",
|
|
174
|
+
variable=self._type,
|
|
175
|
+
value="fake",
|
|
176
|
+
).grid(column=0, row=4, sticky="we")
|
|
143
177
|
|
|
144
178
|
# ---
|
|
145
179
|
|
|
146
180
|
# New Scale Settings.
|
|
147
|
-
new_scale_settings = ttk.LabelFrame(mainframe, text="Pathfinder Settings", padding=3)
|
|
181
|
+
new_scale_settings = ttk.LabelFrame(mainframe, text="Pathfinder MPM Settings", padding=3)
|
|
148
182
|
new_scale_settings.grid(column=0, row=2, sticky="news")
|
|
149
183
|
|
|
150
184
|
# Port
|
|
151
185
|
ttk.Label(new_scale_settings, text="HTTP Server Port:", anchor=E, justify=RIGHT).grid(
|
|
152
186
|
column=0, row=1, sticky="we"
|
|
153
187
|
)
|
|
154
|
-
ttk.Entry(new_scale_settings, textvariable=self.
|
|
188
|
+
ttk.Entry(new_scale_settings, textvariable=self._mpm_port, width=5, justify=CENTER).grid(
|
|
155
189
|
column=1, row=1, sticky="we"
|
|
156
190
|
)
|
|
157
191
|
|
|
@@ -173,45 +207,9 @@ class GUI:
|
|
|
173
207
|
).grid(column=0, row=4, columnspan=2, sticky="we")
|
|
174
208
|
|
|
175
209
|
def _launch_server(self) -> None:
|
|
176
|
-
"""
|
|
210
|
+
"""Close GUI and return to the server.
|
|
177
211
|
|
|
178
|
-
|
|
212
|
+
Options are saved in fields.
|
|
213
|
+
"""
|
|
214
|
+
self._submit = True
|
|
179
215
|
self._root.destroy()
|
|
180
|
-
|
|
181
|
-
# Save settings.
|
|
182
|
-
settings = {
|
|
183
|
-
"ignore_updates": self._ignore_updates.get(),
|
|
184
|
-
"type": self._type.get(),
|
|
185
|
-
"debug": self._debug.get(),
|
|
186
|
-
"proxy": self._proxy.get(),
|
|
187
|
-
"proxy_address": self._proxy_address.get(),
|
|
188
|
-
"port": self._port.get(),
|
|
189
|
-
"pathfinder_port": self._pathfinder_port.get(),
|
|
190
|
-
"serial": self._serial.get(),
|
|
191
|
-
}
|
|
192
|
-
makedirs(SETTINGS_DIR, exist_ok=True)
|
|
193
|
-
with open(f"{SETTINGS_DIR}\\{SETTINGS_FILENAME}", "w+") as f:
|
|
194
|
-
f.write(dumps(settings))
|
|
195
|
-
|
|
196
|
-
# Launch server.
|
|
197
|
-
server = Server()
|
|
198
|
-
|
|
199
|
-
com.DEBUG = self._debug.get()
|
|
200
|
-
|
|
201
|
-
if self._serial.get() != "no-e-stop":
|
|
202
|
-
e_stop = EmergencyStop(server, self._serial.get())
|
|
203
|
-
e_stop.watch()
|
|
204
|
-
|
|
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())
|
|
File without changes
|
|
@@ -0,0 +1,133 @@
|
|
|
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: Implement the BaseBindings class when defining a platform binding to ensure it supports the necessary methods.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
|
|
10
|
+
from vbl_aquarium.models.unity import Vector3, Vector4
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BaseBindings(ABC):
|
|
14
|
+
"""Base class to enforce bindings manipulator platforms will support.
|
|
15
|
+
|
|
16
|
+
No need to catch exceptions as the Platform Handler will catch them.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
async def get_manipulators(self) -> list[str]:
|
|
21
|
+
"""Get a list of available manipulators on the current platform.
|
|
22
|
+
|
|
23
|
+
:returns: List of manipulator IDs.
|
|
24
|
+
:rtype: list[str]
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
async def get_num_axes(self) -> int:
|
|
29
|
+
"""Get the number of axes for the current platform.
|
|
30
|
+
|
|
31
|
+
:returns: Number of axes.
|
|
32
|
+
:rtype: int
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def get_dimensions(self) -> Vector4:
|
|
37
|
+
"""Get the dimensions of the manipulators on the current platform (mm).
|
|
38
|
+
|
|
39
|
+
For 3-axis manipulators, copy the dimension of the axis parallel to the probe into w.
|
|
40
|
+
|
|
41
|
+
:returns: Dimensions of the manipulators.
|
|
42
|
+
:rtype: Vector4
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
@abstractmethod
|
|
46
|
+
async def get_position(self, manipulator_id: str) -> Vector4:
|
|
47
|
+
"""Get the current position of a manipulator.
|
|
48
|
+
|
|
49
|
+
These will be the translation values of the manipulator (mm), so they may need to be rotated to unified space.
|
|
50
|
+
For 3-axis manipulators, copy the position of the axis parallel to the probe into w.
|
|
51
|
+
|
|
52
|
+
:param manipulator_id: Manipulator ID.
|
|
53
|
+
:type manipulator_id: str
|
|
54
|
+
:returns: Current position of the manipulator in platform space (mm).
|
|
55
|
+
:rtype: Vector4
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
async def get_angles(self, manipulator_id: str) -> Vector3:
|
|
60
|
+
"""Get the current rotation angles of a manipulator in Yaw, Pitch, Roll (degrees).
|
|
61
|
+
|
|
62
|
+
:param manipulator_id: Manipulator ID.
|
|
63
|
+
:type manipulator_id: str
|
|
64
|
+
:returns: Current angles of the manipulator.
|
|
65
|
+
:rtype: Vector3
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
@abstractmethod
|
|
69
|
+
async def get_shank_count(self, manipulator_id: str) -> int:
|
|
70
|
+
"""Get the number of shanks on a manipulator.
|
|
71
|
+
|
|
72
|
+
:param manipulator_id: Manipulator ID.
|
|
73
|
+
:type manipulator_id: str
|
|
74
|
+
:returns: Number of shanks on the manipulator.
|
|
75
|
+
:rtype: int
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
@abstractmethod
|
|
79
|
+
async def get_movement_tolerance(self) -> float:
|
|
80
|
+
"""Get the tolerance for how close the final position must be to the target position in a movement (mm).
|
|
81
|
+
|
|
82
|
+
:returns: Movement tolerance (mm).
|
|
83
|
+
:rtype: float
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
@abstractmethod
|
|
87
|
+
async def set_position(self, manipulator_id: str, position: Vector4, speed: float) -> Vector4:
|
|
88
|
+
"""Set the position of a manipulator.
|
|
89
|
+
|
|
90
|
+
This will directly set the position in the original platform space.
|
|
91
|
+
Unified space coordinates will need to be converted to platform space.
|
|
92
|
+
For 3-axis manipulators, the first 3 values of the position will be used.
|
|
93
|
+
|
|
94
|
+
:param manipulator_id: Manipulator ID.
|
|
95
|
+
:type manipulator_id: str
|
|
96
|
+
:param position: Platform space position to set the manipulator to (mm).
|
|
97
|
+
:type position: Vector4
|
|
98
|
+
:param speed: Speed to move the manipulator to the position (mm/s).
|
|
99
|
+
:type speed: float
|
|
100
|
+
:returns: Final position of the manipulator in platform space (mm).
|
|
101
|
+
:rtype: Vector4
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
@abstractmethod
|
|
105
|
+
async def stop(self, manipulator_id: str) -> None:
|
|
106
|
+
"""Stop a manipulator."""
|
|
107
|
+
|
|
108
|
+
@abstractmethod
|
|
109
|
+
def platform_space_to_unified_space(self, platform_space: Vector4) -> Vector4:
|
|
110
|
+
"""Convert platform space coordinates to unified space coordinates.
|
|
111
|
+
|
|
112
|
+
This is an axes-swapping transformation.
|
|
113
|
+
|
|
114
|
+
Unified coordinate space is the standard left-handed cartesian coordinate system
|
|
115
|
+
with an additional depth axis pointing from the base of the probe to the tip.
|
|
116
|
+
|
|
117
|
+
:param platform_space: Platform space coordinates.
|
|
118
|
+
:type platform_space: Vector4
|
|
119
|
+
:returns: Unified space coordinates.
|
|
120
|
+
:rtype: Vector4
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
@abstractmethod
|
|
124
|
+
def unified_space_to_platform_space(self, unified_space: Vector4) -> Vector4:
|
|
125
|
+
"""Convert unified space coordinates to platform space coordinates.
|
|
126
|
+
|
|
127
|
+
This is an axes-swapping transformation.
|
|
128
|
+
|
|
129
|
+
:param unified_space: Unified space coordinates.
|
|
130
|
+
:type unified_space: Vector4
|
|
131
|
+
:returns: Platform space coordinates.
|
|
132
|
+
:rtype: Vector4
|
|
133
|
+
"""
|