ephys-link 2.0.0b1__py3-none-any.whl → 2.0.0b5__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 CHANGED
@@ -1 +1 @@
1
- __version__ = "2.0.0b1"
1
+ __version__ = "2.0.0b5"
ephys_link/__main__.py CHANGED
@@ -31,7 +31,7 @@ def main() -> None:
31
31
  console = Console(enable_debug=options.debug)
32
32
 
33
33
  # 3. Instantiate the Platform Handler with the appropriate platform bindings.
34
- platform_handler = PlatformHandler(options.type, console)
34
+ platform_handler = PlatformHandler(options, console)
35
35
 
36
36
  # 4. Instantiate the Emergency Stop service.
37
37
 
@@ -12,6 +12,7 @@ from uuid import uuid4
12
12
  from vbl_aquarium.models.ephys_link import (
13
13
  AngularResponse,
14
14
  BooleanStateResponse,
15
+ EphysLinkOptions,
15
16
  GetManipulatorsResponse,
16
17
  PositionalResponse,
17
18
  SetDepthRequest,
@@ -25,6 +26,7 @@ from vbl_aquarium.models.unity import Vector4
25
26
 
26
27
  from ephys_link.__about__ import __version__
27
28
  from ephys_link.bindings.fake_bindings import FakeBindings
29
+ from ephys_link.bindings.mpm_bindings import MPMBinding
28
30
  from ephys_link.bindings.ump_4_bindings import Ump4Bindings
29
31
  from ephys_link.util.base_bindings import BaseBindings
30
32
  from ephys_link.util.common import vector4_to_array
@@ -34,21 +36,21 @@ from ephys_link.util.console import Console
34
36
  class PlatformHandler:
35
37
  """Handler for platform commands."""
36
38
 
37
- def __init__(self, platform_type: str, console: Console) -> None:
39
+ def __init__(self, options: EphysLinkOptions, console: Console) -> None:
38
40
  """Initialize platform handler.
39
41
 
40
- :param platform_type: Platform type to initialize bindings from.
41
- :type platform_type: str
42
+ :param options: CLI options.
43
+ :type options: EphysLinkOptions
42
44
  """
43
45
 
44
- # Store the platform type.
45
- self._platform_type = platform_type
46
+ # Store the CLI options.
47
+ self._options = options
46
48
 
47
49
  # Store the console.
48
50
  self._console = console
49
51
 
50
52
  # Define bindings based on platform type.
51
- self._bindings = self._match_platform_type(platform_type)
53
+ self._bindings = self._match_platform_type(options)
52
54
 
53
55
  # Record which IDs are inside the brain.
54
56
  self._inside_brain: set[str] = set()
@@ -56,22 +58,24 @@ class PlatformHandler:
56
58
  # Generate a Pinpoint ID for proxy usage.
57
59
  self._pinpoint_id = str(uuid4())[:8]
58
60
 
59
- def _match_platform_type(self, platform_type: str) -> BaseBindings:
61
+ def _match_platform_type(self, options: EphysLinkOptions) -> BaseBindings:
60
62
  """Match the platform type to the appropriate bindings.
61
63
 
62
- :param platform_type: Platform type.
63
- :type platform_type: str
64
+ :param options: CLI options.
65
+ :type options: EphysLinkOptions
64
66
  :returns: Bindings for the specified platform type.
65
67
  :rtype: :class:`ephys_link.util.base_bindings.BaseBindings`
66
68
  """
67
- match platform_type:
69
+ match options.type:
68
70
  case "ump-4":
69
71
  return Ump4Bindings()
72
+ case "pathfinder-mpm":
73
+ return MPMBinding(options.mpm_port)
70
74
  case "fake":
71
75
  return FakeBindings()
72
76
  case _:
73
- error_message = f'Platform type "{platform_type}" not recognized.'
74
- self._console.labeled_error_print("PLATFORM", error_message)
77
+ error_message = f'Platform type "{options.type}" not recognized.'
78
+ self._console.critical_print(error_message)
75
79
  raise ValueError(error_message)
76
80
 
77
81
  # Ephys Link metadata.
@@ -99,7 +103,7 @@ class PlatformHandler:
99
103
  :returns: Platform type config identifier (see CLI options for examples).
100
104
  :rtype: str
101
105
  """
102
- return self._platform_type
106
+ return str(self._options.type)
103
107
 
104
108
  # Manipulator commands.
105
109
 
@@ -111,7 +115,7 @@ class PlatformHandler:
111
115
  """
112
116
  try:
113
117
  manipulators = await self._bindings.get_manipulators()
114
- num_axes = await self._bindings.get_num_axes()
118
+ num_axes = await self._bindings.get_axes_count()
115
119
  dimensions = self._bindings.get_dimensions()
116
120
  except Exception as e:
117
121
  self._console.exception_error_print("Get Manipulators", e)
@@ -185,7 +189,7 @@ class PlatformHandler:
185
189
  # Disallow setting manipulator position while inside the brain.
186
190
  if request.manipulator_id in self._inside_brain:
187
191
  error_message = 'Can not move manipulator while inside the brain. Set the depth ("set_depth") instead.'
188
- self._console.error_print(error_message)
192
+ self._console.error_print("Set Position", error_message)
189
193
  return PositionalResponse(error=error_message)
190
194
 
191
195
  # Move to the new position.
@@ -198,18 +202,18 @@ class PlatformHandler:
198
202
 
199
203
  # Return error if movement did not reach target within tolerance.
200
204
  for index, axis in enumerate(vector4_to_array(final_unified_position - request.position)):
201
- # End once index is greater than the number of axes.
202
- if index >= await self._bindings.get_num_axes():
205
+ # End once index is the number of axes.
206
+ if index == await self._bindings.get_axes_count():
203
207
  break
204
208
 
205
209
  # Check if the axis is within the movement tolerance.
206
- if abs(axis) > await self._bindings.get_movement_tolerance():
210
+ if abs(axis) > self._bindings.get_movement_tolerance():
207
211
  error_message = (
208
212
  f"Manipulator {request.manipulator_id} did not reach target"
209
213
  f" position on axis {list(Vector4.model_fields.keys())[index]}."
210
- f"Requested: {request.position}, got: {final_unified_position}."
214
+ f" Requested: {request.position}, got: {final_unified_position}."
211
215
  )
212
- self._console.error_print(error_message)
216
+ self._console.error_print("Set Position", error_message)
213
217
  return PositionalResponse(error=error_message)
214
218
  except Exception as e:
215
219
  self._console.exception_error_print("Set Position", e)
@@ -226,24 +230,27 @@ class PlatformHandler:
226
230
  :rtype: :class:`vbl_aquarium.models.ephys_link.DriveToDepthResponse`
227
231
  """
228
232
  try:
229
- # Create a position based on the new depth.
230
- current_platform_position = await self._bindings.get_position(request.manipulator_id)
231
- current_unified_position = self._bindings.platform_space_to_unified_space(current_platform_position)
232
- target_unified_position = current_unified_position.model_copy(update={"w": request.depth})
233
- target_platform_position = self._bindings.unified_space_to_platform_space(target_unified_position)
234
-
235
233
  # Move to the new depth.
236
- final_platform_position = await self._bindings.set_position(
234
+ final_platform_depth = await self._bindings.set_depth(
237
235
  manipulator_id=request.manipulator_id,
238
- position=target_platform_position,
236
+ depth=self._bindings.unified_space_to_platform_space(Vector4(w=request.depth)).w,
239
237
  speed=request.speed,
240
238
  )
241
- final_unified_position = self._bindings.platform_space_to_unified_space(final_platform_position)
239
+ final_unified_depth = self._bindings.platform_space_to_unified_space(Vector4(w=final_platform_depth)).w
240
+
241
+ # Return error if movement did not reach target within tolerance.
242
+ if abs(final_unified_depth - request.depth) > self._bindings.get_movement_tolerance():
243
+ error_message = (
244
+ f"Manipulator {request.manipulator_id} did not reach target depth."
245
+ f" Requested: {request.depth}, got: {final_unified_depth}."
246
+ )
247
+ self._console.error_print("Set Depth", error_message)
248
+ return SetDepthResponse(error=error_message)
242
249
  except Exception as e:
243
250
  self._console.exception_error_print("Set Depth", e)
244
251
  return SetDepthResponse(error=self._console.pretty_exception(e))
245
252
  else:
246
- return SetDepthResponse(depth=final_unified_position.w)
253
+ return SetDepthResponse(depth=final_unified_depth)
247
254
 
248
255
  async def set_inside_brain(self, request: SetInsideBrainRequest) -> BooleanStateResponse:
249
256
  """Mark a manipulator as inside the brain or not.
@@ -12,7 +12,7 @@ from vbl_aquarium.models.ephys_link import (
12
12
  SetInsideBrainRequest,
13
13
  SetPositionRequest,
14
14
  )
15
- from vbl_aquarium.models.generic import VBLBaseModel
15
+ from vbl_aquarium.utils.vbl_base_model import VBLBaseModel
16
16
 
17
17
  from ephys_link.back_end.platform_handler import PlatformHandler
18
18
  from ephys_link.util.common import PORT, check_for_updates, server_preamble
@@ -75,7 +75,7 @@ class Server:
75
75
  # Helper functions.
76
76
  def _malformed_request_response(self, request: str, data: tuple[tuple[Any], ...]) -> str:
77
77
  """Return a response for a malformed request."""
78
- self._console.labeled_error_print("MALFORMED REQUEST", f"{request}: {data}")
78
+ self._console.error_print("MALFORMED REQUEST", f"{request}: {data}")
79
79
  return dumps({"error": "Malformed request."})
80
80
 
81
81
  async def _run_if_data_available(
@@ -127,7 +127,9 @@ class Server:
127
127
  self._console.info_print("CONNECTION GRANTED", sid)
128
128
  return True
129
129
 
130
- self._console.error_print(f"CONNECTION REFUSED to {sid}. Client {self._client_sid} already connected.")
130
+ self._console.error_print(
131
+ "CONNECTION REFUSED", f"Cannot connect {sid} as {self._client_sid} is already connected."
132
+ )
131
133
  return False
132
134
 
133
135
  async def disconnect(self, sid: str) -> None:
@@ -142,7 +144,7 @@ class Server:
142
144
  if self._client_sid == sid:
143
145
  self._client_sid = ""
144
146
  else:
145
- self._console.error_print(f"Client {sid} disconnected without being connected.")
147
+ self._console.error_print("DISCONNECTION", f"Client {sid} disconnected without being connected.")
146
148
 
147
149
  # noinspection PyTypeChecker
148
150
  async def platform_event_handler(self, event: str, *args: tuple[Any]) -> str:
@@ -196,5 +198,5 @@ class Server:
196
198
  case "stop_all":
197
199
  return await self._platform_handler.stop_all()
198
200
  case _:
199
- self._console.error_print(f"Unknown event: {event}.")
201
+ self._console.error_print("EVENT", f"Unknown event: {event}.")
200
202
  return dumps({"error": "Unknown event."})
@@ -1,6 +1,7 @@
1
1
  from vbl_aquarium.models.unity import Vector3, Vector4
2
2
 
3
3
  from ephys_link.util.base_bindings import BaseBindings
4
+ from ephys_link.util.common import array_to_vector4
4
5
 
5
6
 
6
7
  class FakeBindings(BaseBindings):
@@ -22,11 +23,11 @@ class FakeBindings(BaseBindings):
22
23
  async def get_manipulators(self) -> list[str]:
23
24
  return list(map(str, range(8)))
24
25
 
25
- async def get_num_axes(self) -> int:
26
+ async def get_axes_count(self) -> int:
26
27
  return 4
27
28
 
28
29
  def get_dimensions(self) -> Vector4:
29
- return Vector4(x=20, y=20, z=20, w=20)
30
+ return array_to_vector4([20] * 4)
30
31
 
31
32
  async def get_position(self, manipulator_id: str) -> Vector4:
32
33
  return self._positions[int(manipulator_id)]
@@ -37,13 +38,17 @@ class FakeBindings(BaseBindings):
37
38
  async def get_shank_count(self, _: str) -> int:
38
39
  return 1
39
40
 
40
- async def get_movement_tolerance(self) -> float:
41
+ def get_movement_tolerance(self) -> float:
41
42
  return 0.001
42
43
 
43
44
  async def set_position(self, manipulator_id: str, position: Vector4, _: float) -> Vector4:
44
45
  self._positions[int(manipulator_id)] = position
45
46
  return position
46
47
 
48
+ async def set_depth(self, manipulator_id: str, depth: float, _: float) -> float:
49
+ self._positions[int(manipulator_id)].w = depth
50
+ return depth
51
+
47
52
  async def stop(self, _: str) -> None:
48
53
  pass
49
54
 
@@ -0,0 +1,278 @@
1
+ """Bindings for New Scale Pathfinder MPM HTTP server platform.
2
+
3
+ MPM works slightly differently than the other platforms since it operates in stereotactic coordinates.
4
+ This means exceptions need to be made for its API.
5
+
6
+ Usage: Instantiate MPMBindings to interact with the New Scale Pathfinder MPM HTTP server platform.
7
+ """
8
+
9
+ from asyncio import get_running_loop, sleep
10
+ from json import dumps
11
+ from typing import Any
12
+
13
+ from requests import JSONDecodeError, get, put
14
+ from vbl_aquarium.models.unity import Vector3, Vector4
15
+
16
+ from ephys_link.util.base_bindings import BaseBindings
17
+ from ephys_link.util.common import scalar_mm_to_um, vector4_to_array
18
+
19
+
20
+ class MPMBinding(BaseBindings):
21
+ """Bindings for New Scale Pathfinder MPM HTTP server platform."""
22
+
23
+ # Valid New Scale manipulator IDs
24
+ VALID_MANIPULATOR_IDS = (
25
+ "A",
26
+ "B",
27
+ "C",
28
+ "D",
29
+ "E",
30
+ "F",
31
+ "G",
32
+ "H",
33
+ "I",
34
+ "J",
35
+ "K",
36
+ "L",
37
+ "M",
38
+ "N",
39
+ "O",
40
+ "P",
41
+ "Q",
42
+ "R",
43
+ "S",
44
+ "T",
45
+ "U",
46
+ "V",
47
+ "W",
48
+ "X",
49
+ "Y",
50
+ "Z",
51
+ "AA",
52
+ "AB",
53
+ "AC",
54
+ "AD",
55
+ "AE",
56
+ "AF",
57
+ "AG",
58
+ "AH",
59
+ "AI",
60
+ "AJ",
61
+ "AK",
62
+ "AL",
63
+ "AM",
64
+ "AN",
65
+ )
66
+
67
+ # Movement polling preferences.
68
+ UNCHANGED_COUNTER_LIMIT = 10
69
+ POLL_INTERVAL = 0.1
70
+
71
+ # Speed preferences (mm/s to use coarse mode).
72
+ COARSE_SPEED_THRESHOLD = 0.1
73
+ INSERTION_SPEED_LIMIT = 9_000
74
+
75
+ def __init__(self, port: int) -> None:
76
+ """Initialize connection to MPM HTTP server.
77
+
78
+ :param port: Port number for MPM HTTP server.
79
+ :type port: int
80
+ """
81
+ self._url = f"http://localhost:{port}"
82
+ self._movement_stopped = False
83
+
84
+ async def get_manipulators(self) -> list[str]:
85
+ return [manipulator["Id"] for manipulator in (await self._query_data())["ProbeArray"]]
86
+
87
+ async def get_axes_count(self) -> int:
88
+ return 3
89
+
90
+ def get_dimensions(self) -> Vector4:
91
+ return Vector4(x=15, y=15, z=15, w=15)
92
+
93
+ async def get_position(self, manipulator_id: str) -> Vector4:
94
+ manipulator_data = await self._manipulator_data(manipulator_id)
95
+ stage_z = manipulator_data["Stage_Z"]
96
+
97
+ await sleep(self.POLL_INTERVAL) # Wait for the stage to stabilize.
98
+
99
+ return Vector4(
100
+ x=manipulator_data["Stage_X"],
101
+ y=manipulator_data["Stage_Y"],
102
+ z=stage_z,
103
+ w=stage_z,
104
+ )
105
+
106
+ async def get_angles(self, manipulator_id: str) -> Vector3:
107
+ manipulator_data = await self._manipulator_data(manipulator_id)
108
+
109
+ # Apply PosteriorAngle to Polar to get the correct angle.
110
+ adjusted_polar = manipulator_data["Polar"] - (await self._query_data())["PosteriorAngle"]
111
+
112
+ return Vector3(
113
+ x=adjusted_polar if adjusted_polar > 0 else 360 + adjusted_polar,
114
+ y=manipulator_data["Pitch"],
115
+ z=manipulator_data["ShankOrientation"],
116
+ )
117
+
118
+ async def get_shank_count(self, manipulator_id: str) -> int:
119
+ return int((await self._manipulator_data(manipulator_id))["ShankCount"])
120
+
121
+ def get_movement_tolerance(self) -> float:
122
+ return 0.01
123
+
124
+ async def set_position(self, manipulator_id: str, position: Vector4, speed: float) -> Vector4:
125
+ # Keep track of the previous position to check if the manipulator stopped advancing.
126
+ current_position = await self.get_position(manipulator_id)
127
+ previous_position = current_position
128
+ unchanged_counter = 0
129
+
130
+ # Set step mode based on speed.
131
+ await self._put_request(
132
+ {
133
+ "PutId": "ProbeStepMode",
134
+ "Probe": self.VALID_MANIPULATOR_IDS.index(manipulator_id),
135
+ "StepMode": 0 if speed > self.COARSE_SPEED_THRESHOLD else 1,
136
+ }
137
+ )
138
+
139
+ # Send move request.
140
+ await self._put_request(
141
+ {
142
+ "PutId": "ProbeMotion",
143
+ "Probe": self.VALID_MANIPULATOR_IDS.index(manipulator_id),
144
+ "Absolute": 1,
145
+ "Stereotactic": 0,
146
+ "AxisMask": 7,
147
+ "X": position.x,
148
+ "Y": position.y,
149
+ "Z": position.z,
150
+ }
151
+ )
152
+
153
+ # Wait for the manipulator to reach the target position or be stopped or stuck.
154
+ while (
155
+ not self._movement_stopped
156
+ and not self._is_vector_close(current_position, position)
157
+ and unchanged_counter < self.UNCHANGED_COUNTER_LIMIT
158
+ ):
159
+ # Wait for a short time before checking again.
160
+ await sleep(self.POLL_INTERVAL)
161
+
162
+ # Update current position.
163
+ current_position = await self.get_position(manipulator_id)
164
+
165
+ # Check if manipulator is not moving.
166
+ if self._is_vector_close(previous_position, current_position):
167
+ # Position did not change.
168
+ unchanged_counter += 1
169
+ else:
170
+ # Position changed.
171
+ unchanged_counter = 0
172
+ previous_position = current_position
173
+
174
+ # Reset movement stopped flag.
175
+ self._movement_stopped = False
176
+
177
+ # Return the final position.
178
+ return await self.get_position(manipulator_id)
179
+
180
+ async def set_depth(self, manipulator_id: str, depth: float, speed: float) -> float:
181
+ # Keep track of the previous depth to check if the manipulator stopped advancing unexpectedly.
182
+ current_depth = (await self.get_position(manipulator_id)).w
183
+ previous_depth = current_depth
184
+ unchanged_counter = 0
185
+
186
+ # Send move request.
187
+ # Convert mm/s to um/min and cap speed at the limit.
188
+ await self._put_request(
189
+ {
190
+ "PutId": "ProbeInsertion",
191
+ "Probe": self.VALID_MANIPULATOR_IDS.index(manipulator_id),
192
+ "Distance": scalar_mm_to_um(current_depth - depth),
193
+ "Rate": min(scalar_mm_to_um(speed) * 60, self.INSERTION_SPEED_LIMIT),
194
+ }
195
+ )
196
+
197
+ # Wait for the manipulator to reach the target depth or be stopped or get stuck.
198
+ while not self._movement_stopped and not abs(current_depth - depth) <= self.get_movement_tolerance():
199
+ # Wait for a short time before checking again.
200
+ await sleep(self.POLL_INTERVAL)
201
+
202
+ # Get the current depth.
203
+ current_depth = (await self.get_position(manipulator_id)).w
204
+
205
+ # Check if manipulator is not moving.
206
+ if abs(previous_depth - current_depth) <= self.get_movement_tolerance():
207
+ # Depth did not change.
208
+ unchanged_counter += 1
209
+ else:
210
+ # Depth changed.
211
+ unchanged_counter = 0
212
+ previous_depth = current_depth
213
+
214
+ # Reset movement stopped flag.
215
+ self._movement_stopped = False
216
+
217
+ # Return the final depth.
218
+ return float((await self.get_position(manipulator_id)).w)
219
+
220
+ async def stop(self, manipulator_id: str) -> None:
221
+ request = {"PutId": "ProbeStop", "Probe": self.VALID_MANIPULATOR_IDS.index(manipulator_id)}
222
+ await self._put_request(request)
223
+ self._movement_stopped = True
224
+
225
+ def platform_space_to_unified_space(self, platform_space: Vector4) -> Vector4:
226
+ # unified <- platform
227
+ # +x <- -x
228
+ # +y <- +z
229
+ # +z <- +y
230
+ # +w <- -w
231
+
232
+ return Vector4(
233
+ x=self.get_dimensions().x - platform_space.x,
234
+ y=platform_space.z,
235
+ z=platform_space.y,
236
+ w=self.get_dimensions().w - platform_space.w,
237
+ )
238
+
239
+ def unified_space_to_platform_space(self, unified_space: Vector4) -> Vector4:
240
+ # platform <- unified
241
+ # +x <- -x
242
+ # +y <- +z
243
+ # +z <- +y
244
+ # +w <- -w
245
+
246
+ return Vector4(
247
+ x=self.get_dimensions().x - unified_space.x,
248
+ y=unified_space.z,
249
+ z=unified_space.y,
250
+ w=self.get_dimensions().w - unified_space.w,
251
+ )
252
+
253
+ # Helper functions.
254
+ async def _query_data(self) -> Any:
255
+ try:
256
+ return (await get_running_loop().run_in_executor(None, get, self._url)).json()
257
+ except ConnectionError as connectionError:
258
+ error_message = f"Unable to connect to MPM HTTP server: {connectionError}"
259
+ raise RuntimeError(error_message) from connectionError
260
+ except JSONDecodeError as jsonDecodeError:
261
+ error_message = f"Unable to decode JSON response from MPM HTTP server: {jsonDecodeError}"
262
+ raise ValueError(error_message) from jsonDecodeError
263
+
264
+ async def _manipulator_data(self, manipulator_id: str) -> Any:
265
+ probe_data = (await self._query_data())["ProbeArray"]
266
+ for probe in probe_data:
267
+ if probe["Id"] == manipulator_id:
268
+ return probe
269
+
270
+ # If we get here, that means the manipulator doesn't exist.
271
+ error_message = f"Manipulator {manipulator_id} not found."
272
+ raise ValueError(error_message)
273
+
274
+ async def _put_request(self, request: dict[str, Any]) -> None:
275
+ await get_running_loop().run_in_executor(None, put, self._url, dumps(request))
276
+
277
+ def _is_vector_close(self, target: Vector4, current: Vector4) -> bool:
278
+ return all(abs(axis) <= self.get_movement_tolerance() for axis in vector4_to_array(target - current)[:3])
@@ -9,8 +9,14 @@ from sensapex import UMP, SensapexDevice
9
9
  from vbl_aquarium.models.unity import Vector3, Vector4
10
10
 
11
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
12
+ from ephys_link.util.common import (
13
+ RESOURCES_PATH,
14
+ array_to_vector4,
15
+ scalar_mm_to_um,
16
+ um_to_mm,
17
+ vector4_to_array,
18
+ vector_mm_to_um,
19
+ )
14
20
 
15
21
 
16
22
  class Ump4Bindings(BaseBindings):
@@ -24,13 +30,12 @@ class Ump4Bindings(BaseBindings):
24
30
  self._ump = UMP.get_ump()
25
31
  if self._ump is None:
26
32
  error_message = "Unable to connect to uMp"
27
- Console.error_print(error_message)
28
33
  raise ValueError(error_message)
29
34
 
30
35
  async def get_manipulators(self) -> list[str]:
31
36
  return list(map(str, self._ump.list_devices()))
32
37
 
33
- async def get_num_axes(self) -> int:
38
+ async def get_axes_count(self) -> int:
34
39
  return 4
35
40
 
36
41
  def get_dimensions(self) -> Vector4:
@@ -57,29 +62,17 @@ class Ump4Bindings(BaseBindings):
57
62
  error_message = "UMP-4 does not support getting shank count"
58
63
  raise AttributeError(error_message)
59
64
 
60
- async def get_movement_tolerance(self) -> float:
65
+ def get_movement_tolerance(self) -> float:
61
66
  return 0.001
62
67
 
63
68
  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
69
  # Convert position to micrometers.
79
- target_position_um = mm_to_um(position)
70
+ target_position_um = vector_mm_to_um(position)
80
71
 
81
72
  # Request movement.
82
- movement = self._get_device(manipulator_id).goto_pos(vector4_to_array(target_position_um), mmps_to_umps(speed))
73
+ movement = self._get_device(manipulator_id).goto_pos(
74
+ vector4_to_array(target_position_um), scalar_mm_to_um(speed)
75
+ )
83
76
 
84
77
  # Wait for movement to finish.
85
78
  await get_running_loop().run_in_executor(None, movement.finished_event.wait)
@@ -91,6 +84,17 @@ class Ump4Bindings(BaseBindings):
91
84
 
92
85
  return um_to_mm(array_to_vector4(movement.last_pos))
93
86
 
87
+ async def set_depth(self, manipulator_id: str, depth: float, speed: float) -> float:
88
+ # Augment current position with depth.
89
+ current_position = await self.get_position(manipulator_id)
90
+ new_platform_position = current_position.model_copy(update={"w": depth})
91
+
92
+ # Make the movement.
93
+ final_platform_position = await self.set_position(manipulator_id, new_platform_position, speed)
94
+
95
+ # Return the final depth.
96
+ return float(final_platform_position.w)
97
+
94
98
  async def stop(self, manipulator_id: str) -> None:
95
99
  self._get_device(manipulator_id).stop()
96
100
 
@@ -25,7 +25,7 @@ class BaseBindings(ABC):
25
25
  """
26
26
 
27
27
  @abstractmethod
28
- async def get_num_axes(self) -> int:
28
+ async def get_axes_count(self) -> int:
29
29
  """Get the number of axes for the current platform.
30
30
 
31
31
  :returns: Number of axes.
@@ -76,7 +76,7 @@ class BaseBindings(ABC):
76
76
  """
77
77
 
78
78
  @abstractmethod
79
- async def get_movement_tolerance(self) -> float:
79
+ def get_movement_tolerance(self) -> float:
80
80
  """Get the tolerance for how close the final position must be to the target position in a movement (mm).
81
81
 
82
82
  :returns: Movement tolerance (mm).
@@ -88,7 +88,6 @@ class BaseBindings(ABC):
88
88
  """Set the position of a manipulator.
89
89
 
90
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
91
  For 3-axis manipulators, the first 3 values of the position will be used.
93
92
 
94
93
  :param manipulator_id: Manipulator ID.
@@ -101,6 +100,22 @@ class BaseBindings(ABC):
101
100
  :rtype: Vector4
102
101
  """
103
102
 
103
+ @abstractmethod
104
+ async def set_depth(self, manipulator_id: str, depth: float, speed: float) -> float:
105
+ """Set the depth of a manipulator.
106
+
107
+ This will directly set the depth stage in the original platform space.
108
+
109
+ :param manipulator_id: Manipulator ID.
110
+ :type manipulator_id: str
111
+ :param depth: Depth to set the manipulator to (mm).
112
+ :type depth: float
113
+ :param speed: Speed to move the manipulator to the depth (mm/s).
114
+ :type speed: float
115
+ :returns: Final depth of the manipulator in platform space (mm).
116
+ :rtype: float
117
+ """
118
+
104
119
  @abstractmethod
105
120
  async def stop(self, manipulator_id: str) -> None:
106
121
  """Stop a manipulator."""
ephys_link/util/common.py CHANGED
@@ -9,7 +9,6 @@ from requests import get
9
9
  from vbl_aquarium.models.unity import Vector4
10
10
 
11
11
  from ephys_link.__about__ import __version__
12
- from ephys_link.util.console import Console
13
12
 
14
13
  # Ephys Link ASCII.
15
14
  ASCII = r"""
@@ -47,30 +46,30 @@ def check_for_updates() -> None:
47
46
  response = get("https://api.github.com/repos/VirtualBrainLab/ephys-link/tags", timeout=10)
48
47
  latest_version = response.json()[0]["name"]
49
48
  if parse(latest_version) > parse(__version__):
50
- Console.info_print("Update available", latest_version)
51
- Console.info_print("", "Download at: https://github.com/VirtualBrainLab/ephys-link/releases/latest")
49
+ print(f"Update available: {latest_version} !")
50
+ print("Download at: https://github.com/VirtualBrainLab/ephys-link/releases/latest")
52
51
 
53
52
 
54
53
  # Unit conversions
55
54
 
56
55
 
57
- def mmps_to_umps(mmps: float) -> float:
58
- """Convert millimeters per second to micrometers per second.
56
+ def scalar_mm_to_um(mm: float) -> float:
57
+ """Convert scalar values of millimeters to micrometers.
59
58
 
60
- :param mmps: Speed in millimeters per second.
61
- :type mmps: float
62
- :returns: Speed in micrometers per second.
59
+ :param mm: Scalar value in millimeters.
60
+ :type mm: float
61
+ :returns: Scalar value in micrometers.
63
62
  :rtype: float
64
63
  """
65
- return mmps * 1_000
64
+ return mm * 1_000
66
65
 
67
66
 
68
- def mm_to_um(mm: Vector4) -> Vector4:
69
- """Convert millimeters to micrometers.
67
+ def vector_mm_to_um(mm: Vector4) -> Vector4:
68
+ """Convert vector values of millimeters to micrometers.
70
69
 
71
- :param mm: Length in millimeters.
70
+ :param mm: Vector in millimeters.
72
71
  :type mm: Vector4
73
- :returns: Length in micrometers.
72
+ :returns: Vector in micrometers.
74
73
  :rtype: Vector4
75
74
  """
76
75
  return mm * 1_000
@@ -6,12 +6,10 @@ Configure the console to print error and debug messages.
6
6
  Usage: Create a Console object and call the appropriate method to print messages.
7
7
  """
8
8
 
9
- from traceback import print_exc
9
+ from logging import DEBUG, ERROR, INFO, basicConfig, getLogger
10
10
 
11
- from colorama import Back, Fore, Style, init
12
-
13
- # Constants.
14
- TAB_BLOCK = "\t\t"
11
+ from rich.logging import RichHandler
12
+ from rich.traceback import install
15
13
 
16
14
 
17
15
  class Console:
@@ -21,34 +19,59 @@ class Console:
21
19
  :param enable_debug: Enable debug mode.
22
20
  :type enable_debug: bool
23
21
  """
24
- self._enable_debug = enable_debug
25
-
26
22
  # Repeat message fields.
27
- self._last_message = ""
28
- self._repeat_counter = 1
23
+ self._last_message = (0, "", "")
24
+ self._repeat_counter = 0
29
25
 
30
- # Initialize colorama.
31
- init(autoreset=True)
26
+ # Config logger.
27
+ basicConfig(
28
+ format="%(message)s",
29
+ datefmt="[%I:%M:%S %p]",
30
+ handlers=[RichHandler(rich_tracebacks=True, markup=True)],
31
+ )
32
+ self._log = getLogger("rich")
33
+ self._log.setLevel(DEBUG if enable_debug else INFO)
32
34
 
33
- @staticmethod
34
- def error_print(msg: str) -> None:
35
- """Print an error message to the console.
35
+ # Install Rich traceback.
36
+ install()
36
37
 
37
- :param msg: Error message to print.
38
+ def debug_print(self, label: str, msg: str) -> None:
39
+ """Print a debug message to the console.
40
+
41
+ :param label: Label for the debug message.
42
+ :type label: str
43
+ :param msg: Debug message to print.
38
44
  :type msg: str
39
45
  """
40
- print(f"\n{Back.RED}{Style.BRIGHT} ERROR {Style.RESET_ALL}{TAB_BLOCK}{Fore.RED}{msg}")
46
+ self._repeatable_log(DEBUG, f"[b green]{label}", f"[green]{msg}")
41
47
 
42
- @staticmethod
43
- def labeled_error_print(label: str, msg: str) -> None:
44
- """Print an error message with a label to the console.
48
+ def info_print(self, label: str, msg: str) -> None:
49
+ """Print info to console.
50
+
51
+ :param label: Label for the message.
52
+ :type label: str
53
+ :param msg: Message to print.
54
+ :type msg: str
55
+ """
56
+ self._repeatable_log(INFO, f"[b blue]{label}", msg)
57
+
58
+ def error_print(self, label: str, msg: str) -> None:
59
+ """Print an error message to the console.
45
60
 
46
61
  :param label: Label for the error message.
47
62
  :type label: str
48
63
  :param msg: Error message to print.
49
64
  :type msg: str
50
65
  """
51
- print(f"\n{Back.RED}{Style.BRIGHT} ERROR {label} {Style.RESET_ALL}{TAB_BLOCK}{Fore.RED}{msg}")
66
+ self._repeatable_log(ERROR, f"[b red]{label}", f"[red]{msg}")
67
+
68
+ def critical_print(self, msg: str) -> None:
69
+ """Print a critical message to the console.
70
+
71
+ :param msg: Critical message to print.
72
+ :type msg: str
73
+ """
74
+ self._log.critical(f"[b i red]{msg}")
52
75
 
53
76
  @staticmethod
54
77
  def pretty_exception(exception: Exception) -> str:
@@ -61,8 +84,7 @@ class Console:
61
84
  """
62
85
  return f"{type(exception).__name__}: {exception}"
63
86
 
64
- @staticmethod
65
- def exception_error_print(label: str, exception: Exception) -> None:
87
+ def exception_error_print(self, label: str, exception: Exception) -> None:
66
88
  """Print an error message with exception details to the console.
67
89
 
68
90
  :param label: Label for the error message.
@@ -70,43 +92,39 @@ class Console:
70
92
  :param exception: Exception to print.
71
93
  :type exception: Exception
72
94
  """
73
- Console.labeled_error_print(label, Console.pretty_exception(exception))
74
- print_exc()
75
-
76
- def debug_print(self, label: str, msg: str) -> None:
77
- """Print a debug message to the console.
78
-
79
- :param label: Label for the debug message.
80
- :type label: str
81
- :param msg: Debug message to print.
82
- :type msg: str
83
- """
84
- if self._enable_debug:
85
- self._repeat_print(f"{Back.BLUE}{Style.BRIGHT} DEBUG {label} {Style.RESET_ALL}{TAB_BLOCK}{Fore.BLUE}{msg}")
95
+ self._log.exception(f"[b magenta]{label}:[/] [magenta]{Console.pretty_exception(exception)}")
86
96
 
87
- @staticmethod
88
- def info_print(label: str, msg: str) -> None:
89
- """Print info to console.
97
+ # Helper methods.
98
+ def _repeatable_log(self, log_type: int, label: str, message: str) -> None:
99
+ """Add a row to the output table.
90
100
 
101
+ :param log_type: Type of log.
102
+ :type log_type: int
91
103
  :param label: Label for the message.
92
104
  :type label: str
93
- :param msg: Message to print.
94
- :type msg: str
105
+ :param message: Message.
106
+ :type message: str
95
107
  """
96
- print(f"\n{Back.GREEN}{Style.BRIGHT} {label} {Style.RESET_ALL}{TAB_BLOCK}{Fore.GREEN}{msg}")
97
-
98
- # Helper methods.
99
- def _repeat_print(self, msg: str) -> None:
100
- """Print a message to the console with repeat counter.
101
108
 
102
- :param msg: Message to print.
103
- :type msg: str
104
- """
105
- if msg == self._last_message:
109
+ # Compute if this is a repeated message.
110
+ message_set = (log_type, label, message)
111
+ if message_set == self._last_message:
112
+ # Handle repeat.
106
113
  self._repeat_counter += 1
107
- else:
108
- self._repeat_counter = 1
109
- self._last_message = msg
110
- print()
111
114
 
112
- print(f"\r{msg}{f" (x{self._repeat_counter})" if self._repeat_counter > 1 else ""}", end="")
115
+ # Add an ellipsis row for first repeat.
116
+ if self._repeat_counter == 1:
117
+ self._log.log(log_type, "...")
118
+ else:
119
+ # Handle novel message.
120
+ if self._repeat_counter > 0:
121
+ # Complete previous repeat.
122
+ self._log.log(
123
+ self._last_message[0],
124
+ f"{self._last_message[1]}:[/] {self._last_message[2]}[/] x {self._repeat_counter}",
125
+ )
126
+ self._repeat_counter = 0
127
+
128
+ # Log new message.
129
+ self._log.log(log_type, f"{label}:[/] {message}")
130
+ self._last_message = message_set
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ephys-link
3
- Version: 2.0.0b1
3
+ Version: 2.0.0b5
4
4
  Summary: A Python Socket.IO server that allows any Socket.IO-compliant application to communicate with manipulators used in electrophysiology experiments.
5
5
  Project-URL: Documentation, https://virtualbrainlab.org/ephys_link/installation_and_use.html
6
6
  Project-URL: Issues, https://github.com/VirtualBrainLab/ephys-link/issues
@@ -33,8 +33,9 @@ Requires-Dist: pyserial==3.5
33
33
  Requires-Dist: python-socketio[asyncio-client]==5.11.3
34
34
  Requires-Dist: pythonnet==3.0.3
35
35
  Requires-Dist: requests==2.32.3
36
+ Requires-Dist: rich==13.7.1
36
37
  Requires-Dist: sensapex==1.400.1
37
- Requires-Dist: vbl-aquarium==0.0.19
38
+ Requires-Dist: vbl-aquarium==0.0.22
38
39
  Description-Content-Type: text/markdown
39
40
 
40
41
  # Electrophysiology Manipulator Link
@@ -119,13 +120,12 @@ window instead of `localhost`.
119
120
  pip install ephys-link
120
121
  ```
121
122
 
122
- Import the modules you need and launch the server.
123
+ Import main and run (this will launch the setup GUI).
123
124
 
124
125
  ```python
125
- from ephys_link.server import Server
126
+ from ephys_link.__main__ import main
126
127
 
127
- server = Server()
128
- server.launch("sensapex", args.proxy_address, 8081)
128
+ main()
129
129
  ```
130
130
 
131
131
  ## Install for Development
@@ -0,0 +1,26 @@
1
+ ephys_link/__about__.py,sha256=W8_pwZswCUyYGNDMBbTV2lPT7qpoSD2adR4yUwvZfkU,25
2
+ ephys_link/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ ephys_link/__main__.py,sha256=KSwJO4gPQAZitNSNChD1NjkO2j3pc8RA2dkRbiaq32w,1368
4
+ ephys_link/back_end/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ ephys_link/back_end/platform_handler.py,sha256=cAHZ3gqA-Y33vb0sfsH085BPVtIfSUGaFVqvTzpA2MA,13168
6
+ ephys_link/back_end/server.py,sha256=QZu8deE57BxULfUutPQ41q3HTmr94nPyzaIT-JoOK2I,8026
7
+ ephys_link/bindings/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ ephys_link/bindings/fake_bindings.py,sha256=Dk9Bpv4XTYa6ooh5Ge_5-o3IRzdazj5pYMcp3O41UJI,1936
9
+ ephys_link/bindings/mpm_bindings.py,sha256=e4whLU7mJm-Mbw6O2ua6fPHBZNnCiyyGiePEVBr3zzY,9818
10
+ ephys_link/bindings/ump_4_bindings.py,sha256=rmkniiH5KyWjyXGpmUW3RCR_rar40b3vxHwYakrwbqg,4494
11
+ ephys_link/front_end/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ ephys_link/front_end/cli.py,sha256=KJBSWqdz4T5z0Zor1tJSHTJKZeMcHAJf5gXXu38wQPU,3105
13
+ ephys_link/front_end/gui.py,sha256=_gE6zFhFnzHPSyYd9MBvfK5xmDZHsUXcETDHzH66QzU,7518
14
+ ephys_link/resources/CP210xManufacturing.dll,sha256=aM9k_XABjkq0TOMiIw8HeteB40zqEkUDNO8wo91EdYI,810232
15
+ ephys_link/resources/NstMotorCtrl.dll,sha256=Xtpr3vBcxhcsOUGvgVEwYtGPvKEqDctIUGCK36GfU2Q,155136
16
+ ephys_link/resources/SiUSBXp.dll,sha256=187zlclZNNezCkU1o1CbICRAmKWJxbh8ahP6L6wo-_Y,469752
17
+ ephys_link/resources/libum.dll,sha256=YaD4dwiSNohx-XxHjx2eQWPOBEVvUIXARvx37e_yqNw,316316
18
+ ephys_link/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
+ ephys_link/util/base_bindings.py,sha256=ukomZftpwa8s_SV-jcwPSXmox0neXf8-uyie_NmiRro,5349
20
+ ephys_link/util/common.py,sha256=Pk7uVqEMFMPKNpeucWda_GfnHogRszej5G5qYkt43d8,3455
21
+ ephys_link/util/console.py,sha256=NvUH-Fp4nzkgrqQOcylctf46x4AW-qAphrtisepk1xY,4325
22
+ ephys_link-2.0.0b5.dist-info/METADATA,sha256=PvHqwUICg5mNPrvMvmGjpwcDQqQEtDzDacv4AuIsj88,7943
23
+ ephys_link-2.0.0b5.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
24
+ ephys_link-2.0.0b5.dist-info/entry_points.txt,sha256=o8wV3AdnJ9o47vg9ymKxPNVq9pMdPq8UZHE_iyAJx-k,124
25
+ ephys_link-2.0.0b5.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
26
+ ephys_link-2.0.0b5.dist-info/RECORD,,
@@ -1,25 +0,0 @@
1
- ephys_link/__about__.py,sha256=QUUi7DnZ0lfA8ZZUdMrPNiqXewCSfYtIqTvB6q6NnGQ,25
2
- ephys_link/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- ephys_link/__main__.py,sha256=pu7QLmS_30qWpgzeibMVD5FsVhICiK0wi7QkzX6F0qU,1373
4
- ephys_link/back_end/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- ephys_link/back_end/platform_handler.py,sha256=5GfyLz8rX8VYypuT9JJokV12mnyUksriFOzRgp6H4W8,12939
6
- ephys_link/back_end/server.py,sha256=WP5NLJnIKtkVImdRgtZKxWEghd17FjsmpuczYpoFs3Q,7965
7
- ephys_link/bindings/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- ephys_link/bindings/fake_bindings.py,sha256=_ZpXx4whztbO5jNGNqwoJFdqhIIbZ0VMx4mHkMge1r0,1726
9
- ephys_link/bindings/ump_4_bindings.py,sha256=1V_bQzq7xVX1qGxbK11nxX7X3L3cfh9RTdhbVJwBKg8,4651
10
- ephys_link/front_end/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
- ephys_link/front_end/cli.py,sha256=KJBSWqdz4T5z0Zor1tJSHTJKZeMcHAJf5gXXu38wQPU,3105
12
- ephys_link/front_end/gui.py,sha256=_gE6zFhFnzHPSyYd9MBvfK5xmDZHsUXcETDHzH66QzU,7518
13
- ephys_link/resources/CP210xManufacturing.dll,sha256=aM9k_XABjkq0TOMiIw8HeteB40zqEkUDNO8wo91EdYI,810232
14
- ephys_link/resources/NstMotorCtrl.dll,sha256=Xtpr3vBcxhcsOUGvgVEwYtGPvKEqDctIUGCK36GfU2Q,155136
15
- ephys_link/resources/SiUSBXp.dll,sha256=187zlclZNNezCkU1o1CbICRAmKWJxbh8ahP6L6wo-_Y,469752
16
- ephys_link/resources/libum.dll,sha256=YaD4dwiSNohx-XxHjx2eQWPOBEVvUIXARvx37e_yqNw,316316
17
- ephys_link/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
- ephys_link/util/base_bindings.py,sha256=FNqhB7kJf2n9g4wYHEBbQ2wEiZscR5sKCnIEZ6yujgE,4808
19
- ephys_link/util/common.py,sha256=IgmTpXRU2z7jfYV2zB4RGgjV34IaYa2s3TYpzKZkpao,3519
20
- ephys_link/util/console.py,sha256=kD4LLehdA1xLiGwdvjt5KxImDSyDkcoC2CQVOxTv8T4,3560
21
- ephys_link-2.0.0b1.dist-info/METADATA,sha256=j_XqMK_IK955IOE0ikj_JhqschZGCOKpgMiWdGzTrQQ,7975
22
- ephys_link-2.0.0b1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
23
- ephys_link-2.0.0b1.dist-info/entry_points.txt,sha256=o8wV3AdnJ9o47vg9ymKxPNVq9pMdPq8UZHE_iyAJx-k,124
24
- ephys_link-2.0.0b1.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
25
- ephys_link-2.0.0b1.dist-info/RECORD,,