ephys-link 2.0.0b1__tar.gz → 2.0.0b5__tar.gz

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.
Files changed (37) hide show
  1. {ephys_link-2.0.0b1 → ephys_link-2.0.0b5}/PKG-INFO +6 -6
  2. {ephys_link-2.0.0b1 → ephys_link-2.0.0b5}/README.md +3 -4
  3. {ephys_link-2.0.0b1 → ephys_link-2.0.0b5}/pyproject.toml +7 -2
  4. ephys_link-2.0.0b5/qodana.yaml +29 -0
  5. ephys_link-2.0.0b5/scripts/logger_test.py +17 -0
  6. ephys_link-2.0.0b5/scripts/move_tester.py +16 -0
  7. ephys_link-2.0.0b5/src/ephys_link/__about__.py +1 -0
  8. {ephys_link-2.0.0b1 → ephys_link-2.0.0b5}/src/ephys_link/__main__.py +1 -1
  9. {ephys_link-2.0.0b1 → ephys_link-2.0.0b5}/src/ephys_link/back_end/platform_handler.py +37 -30
  10. {ephys_link-2.0.0b1 → ephys_link-2.0.0b5}/src/ephys_link/back_end/server.py +7 -5
  11. {ephys_link-2.0.0b1 → ephys_link-2.0.0b5}/src/ephys_link/bindings/fake_bindings.py +8 -3
  12. ephys_link-2.0.0b5/src/ephys_link/bindings/mpm_bindings.py +278 -0
  13. {ephys_link-2.0.0b1 → ephys_link-2.0.0b5}/src/ephys_link/bindings/ump_4_bindings.py +25 -21
  14. {ephys_link-2.0.0b1 → ephys_link-2.0.0b5}/src/ephys_link/util/base_bindings.py +18 -3
  15. {ephys_link-2.0.0b1 → ephys_link-2.0.0b5}/src/ephys_link/util/common.py +12 -13
  16. ephys_link-2.0.0b5/src/ephys_link/util/console.py +130 -0
  17. ephys_link-2.0.0b1/scripts/move_tester.py +0 -15
  18. ephys_link-2.0.0b1/src/ephys_link/__about__.py +0 -1
  19. ephys_link-2.0.0b1/src/ephys_link/util/console.py +0 -112
  20. {ephys_link-2.0.0b1 → ephys_link-2.0.0b5}/.gitignore +0 -0
  21. {ephys_link-2.0.0b1 → ephys_link-2.0.0b5}/LICENSE +0 -0
  22. {ephys_link-2.0.0b1 → ephys_link-2.0.0b5}/assets/icon.ico +0 -0
  23. {ephys_link-2.0.0b1 → ephys_link-2.0.0b5}/ephys_link.spec +0 -0
  24. {ephys_link-2.0.0b1 → ephys_link-2.0.0b5}/scripts/__init__.py +0 -0
  25. {ephys_link-2.0.0b1 → ephys_link-2.0.0b5}/scripts/server_tester.py +0 -0
  26. {ephys_link-2.0.0b1 → ephys_link-2.0.0b5}/src/ephys_link/__init__.py +0 -0
  27. {ephys_link-2.0.0b1 → ephys_link-2.0.0b5}/src/ephys_link/back_end/__init__.py +0 -0
  28. {ephys_link-2.0.0b1 → ephys_link-2.0.0b5}/src/ephys_link/bindings/__init__.py +0 -0
  29. {ephys_link-2.0.0b1 → ephys_link-2.0.0b5}/src/ephys_link/front_end/__init__.py +0 -0
  30. {ephys_link-2.0.0b1 → ephys_link-2.0.0b5}/src/ephys_link/front_end/cli.py +0 -0
  31. {ephys_link-2.0.0b1 → ephys_link-2.0.0b5}/src/ephys_link/front_end/gui.py +0 -0
  32. {ephys_link-2.0.0b1 → ephys_link-2.0.0b5}/src/ephys_link/resources/CP210xManufacturing.dll +0 -0
  33. {ephys_link-2.0.0b1 → ephys_link-2.0.0b5}/src/ephys_link/resources/NstMotorCtrl.dll +0 -0
  34. {ephys_link-2.0.0b1 → ephys_link-2.0.0b5}/src/ephys_link/resources/SiUSBXp.dll +0 -0
  35. {ephys_link-2.0.0b1 → ephys_link-2.0.0b5}/src/ephys_link/resources/libum.dll +0 -0
  36. {ephys_link-2.0.0b1 → ephys_link-2.0.0b5}/src/ephys_link/util/__init__.py +0 -0
  37. {ephys_link-2.0.0b1 → ephys_link-2.0.0b5}/tests/__init__.py +0 -0
@@ -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
@@ -80,13 +80,12 @@ window instead of `localhost`.
80
80
  pip install ephys-link
81
81
  ```
82
82
 
83
- Import the modules you need and launch the server.
83
+ Import main and run (this will launch the setup GUI).
84
84
 
85
85
  ```python
86
- from ephys_link.server import Server
86
+ from ephys_link.__main__ import main
87
87
 
88
- server = Server()
89
- server.launch("sensapex", args.proxy_address, 8081)
88
+ main()
90
89
  ```
91
90
 
92
91
  ## Install for Development
@@ -38,7 +38,8 @@ dependencies = [
38
38
  "pythonnet==3.0.3",
39
39
  "requests==2.32.3",
40
40
  "sensapex==1.400.1",
41
- "vbl-aquarium==0.0.19"
41
+ "rich==13.7.1",
42
+ "vbl-aquarium==0.0.22"
42
43
  ]
43
44
 
44
45
  [project.urls]
@@ -60,6 +61,7 @@ exclude = ["/.github", "/.idea"]
60
61
  [tool.hatch.envs.default]
61
62
  python = "3.12"
62
63
  dependencies = [
64
+ "mypy",
63
65
  "coverage[toml]>=6.5",
64
66
  "pytest",
65
67
  ]
@@ -114,4 +116,7 @@ exclude_lines = [
114
116
  "no cov",
115
117
  "if __name__ == .__main__.:",
116
118
  "if TYPE_CHECKING:",
117
- ]
119
+ ]
120
+
121
+ [tool.ruff.lint]
122
+ extend-ignore = ["DTZ005"]
@@ -0,0 +1,29 @@
1
+ #-------------------------------------------------------------------------------#
2
+ # Qodana analysis is configured by qodana.yaml file #
3
+ # https://www.jetbrains.com/help/qodana/qodana-yaml.html #
4
+ #-------------------------------------------------------------------------------#
5
+ version: "1.0"
6
+
7
+ #Specify inspection profile for code analysis
8
+ profile:
9
+ name: qodana.starter
10
+
11
+ #Enable inspections
12
+ #include:
13
+ # - name: <SomeEnabledInspectionId>
14
+
15
+ #Disable inspections
16
+ #exclude:
17
+ # - name: <SomeDisabledInspectionId>
18
+ # paths:
19
+ # - <path/where/not/run/inspection>
20
+
21
+ #Execute shell command before Qodana execution (Applied in CI/CD pipeline)
22
+ #bootstrap: sh ./prepare-qodana.sh
23
+
24
+ #Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
25
+ #plugins:
26
+ # - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
27
+
28
+ #Specify Qodana linter for analysis (Applied in CI/CD pipeline)
29
+ linter: jetbrains/qodana-python:latest
@@ -0,0 +1,17 @@
1
+ import logging
2
+
3
+ from rich.logging import RichHandler
4
+
5
+ logging.basicConfig(level="NOTSET", format="%(message)s", datefmt="[%X]", handlers=[RichHandler(rich_tracebacks=True)])
6
+
7
+ log = logging.getLogger("rich")
8
+ log.debug("This message should go to the log file")
9
+ log.info("So should this")
10
+ log.warning("And this, too")
11
+ log.error("And non-ASCII stuff, too, like Øresund and Malmö")
12
+ log.error("[bold red blink]Server is shutting down!", extra={"markup": True})
13
+ log.critical("Critical error! [b red]Server is shutting down!", extra={"markup": True})
14
+ try:
15
+ print(1 / 0)
16
+ except Exception:
17
+ log.exception("[b magenta]unable print![/] [i magenta]asdf", extra={"markup": True})
@@ -0,0 +1,16 @@
1
+ from asyncio import run
2
+
3
+ from vbl_aquarium.models.ephys_link import EphysLinkOptions, SetDepthRequest
4
+ from vbl_aquarium.models.unity import Vector4
5
+
6
+ from ephys_link.back_end.platform_handler import PlatformHandler
7
+ from ephys_link.util.console import Console
8
+
9
+ c = Console(enable_debug=True)
10
+ p = PlatformHandler(EphysLinkOptions(type="pathfinder-mpm"), c)
11
+ # target = Vector4()
12
+ target = Vector4(x=7.5, y=7.5, z=7.5, w=7.5)
13
+
14
+ # print(run(p.set_position(SetPositionRequest(manipulator_id="A", position=target, speed=5))).to_json_string())
15
+ print(run(p.set_depth(SetDepthRequest(manipulator_id="A", depth=7.5, speed=0.15))).to_json_string())
16
+ print("Done!")
@@ -0,0 +1 @@
1
+ __version__ = "2.0.0b5"
@@ -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."""
@@ -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
@@ -0,0 +1,130 @@
1
+ # ruff: noqa: T201
2
+ """Console class for printing messages to the console.
3
+
4
+ Configure the console to print error and debug messages.
5
+
6
+ Usage: Create a Console object and call the appropriate method to print messages.
7
+ """
8
+
9
+ from logging import DEBUG, ERROR, INFO, basicConfig, getLogger
10
+
11
+ from rich.logging import RichHandler
12
+ from rich.traceback import install
13
+
14
+
15
+ class Console:
16
+ def __init__(self, *, enable_debug: bool) -> None:
17
+ """Initialize console properties.
18
+
19
+ :param enable_debug: Enable debug mode.
20
+ :type enable_debug: bool
21
+ """
22
+ # Repeat message fields.
23
+ self._last_message = (0, "", "")
24
+ self._repeat_counter = 0
25
+
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)
34
+
35
+ # Install Rich traceback.
36
+ install()
37
+
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.
44
+ :type msg: str
45
+ """
46
+ self._repeatable_log(DEBUG, f"[b green]{label}", f"[green]{msg}")
47
+
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.
60
+
61
+ :param label: Label for the error message.
62
+ :type label: str
63
+ :param msg: Error message to print.
64
+ :type msg: str
65
+ """
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}")
75
+
76
+ @staticmethod
77
+ def pretty_exception(exception: Exception) -> str:
78
+ """Pretty print an exception.
79
+
80
+ :param exception: Exception to pretty print.
81
+ :type exception: Exception
82
+ :return: Pretty printed exception.
83
+ :rtype: str
84
+ """
85
+ return f"{type(exception).__name__}: {exception}"
86
+
87
+ def exception_error_print(self, label: str, exception: Exception) -> None:
88
+ """Print an error message with exception details to the console.
89
+
90
+ :param label: Label for the error message.
91
+ :type label: str
92
+ :param exception: Exception to print.
93
+ :type exception: Exception
94
+ """
95
+ self._log.exception(f"[b magenta]{label}:[/] [magenta]{Console.pretty_exception(exception)}")
96
+
97
+ # Helper methods.
98
+ def _repeatable_log(self, log_type: int, label: str, message: str) -> None:
99
+ """Add a row to the output table.
100
+
101
+ :param log_type: Type of log.
102
+ :type log_type: int
103
+ :param label: Label for the message.
104
+ :type label: str
105
+ :param message: Message.
106
+ :type message: str
107
+ """
108
+
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.
113
+ self._repeat_counter += 1
114
+
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,15 +0,0 @@
1
- from asyncio import run
2
-
3
- from vbl_aquarium.models.ephys_link import SetPositionRequest
4
- from vbl_aquarium.models.unity import Vector4
5
-
6
- from ephys_link.back_end.platform_handler import PlatformHandler
7
- from ephys_link.util.console import Console
8
-
9
- c = Console(enable_debug=True)
10
- p = PlatformHandler("ump-4", c)
11
- target = Vector4()
12
- # target = Vector4(x=10, y=10, z=10, w=10)
13
-
14
- print(run(p.set_position(SetPositionRequest(manipulator_id="6", position=target, speed=5))).to_json_string())
15
- print("Done!")
@@ -1 +0,0 @@
1
- __version__ = "2.0.0b1"
@@ -1,112 +0,0 @@
1
- # ruff: noqa: T201
2
- """Console class for printing messages to the console.
3
-
4
- Configure the console to print error and debug messages.
5
-
6
- Usage: Create a Console object and call the appropriate method to print messages.
7
- """
8
-
9
- from traceback import print_exc
10
-
11
- from colorama import Back, Fore, Style, init
12
-
13
- # Constants.
14
- TAB_BLOCK = "\t\t"
15
-
16
-
17
- class Console:
18
- def __init__(self, *, enable_debug: bool) -> None:
19
- """Initialize console properties.
20
-
21
- :param enable_debug: Enable debug mode.
22
- :type enable_debug: bool
23
- """
24
- self._enable_debug = enable_debug
25
-
26
- # Repeat message fields.
27
- self._last_message = ""
28
- self._repeat_counter = 1
29
-
30
- # Initialize colorama.
31
- init(autoreset=True)
32
-
33
- @staticmethod
34
- def error_print(msg: str) -> None:
35
- """Print an error message to the console.
36
-
37
- :param msg: Error message to print.
38
- :type msg: str
39
- """
40
- print(f"\n{Back.RED}{Style.BRIGHT} ERROR {Style.RESET_ALL}{TAB_BLOCK}{Fore.RED}{msg}")
41
-
42
- @staticmethod
43
- def labeled_error_print(label: str, msg: str) -> None:
44
- """Print an error message with a label to the console.
45
-
46
- :param label: Label for the error message.
47
- :type label: str
48
- :param msg: Error message to print.
49
- :type msg: str
50
- """
51
- print(f"\n{Back.RED}{Style.BRIGHT} ERROR {label} {Style.RESET_ALL}{TAB_BLOCK}{Fore.RED}{msg}")
52
-
53
- @staticmethod
54
- def pretty_exception(exception: Exception) -> str:
55
- """Pretty print an exception.
56
-
57
- :param exception: Exception to pretty print.
58
- :type exception: Exception
59
- :return: Pretty printed exception.
60
- :rtype: str
61
- """
62
- return f"{type(exception).__name__}: {exception}"
63
-
64
- @staticmethod
65
- def exception_error_print(label: str, exception: Exception) -> None:
66
- """Print an error message with exception details to the console.
67
-
68
- :param label: Label for the error message.
69
- :type label: str
70
- :param exception: Exception to print.
71
- :type exception: Exception
72
- """
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}")
86
-
87
- @staticmethod
88
- def info_print(label: str, msg: str) -> None:
89
- """Print info to console.
90
-
91
- :param label: Label for the message.
92
- :type label: str
93
- :param msg: Message to print.
94
- :type msg: str
95
- """
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
-
102
- :param msg: Message to print.
103
- :type msg: str
104
- """
105
- if msg == self._last_message:
106
- self._repeat_counter += 1
107
- else:
108
- self._repeat_counter = 1
109
- self._last_message = msg
110
- print()
111
-
112
- print(f"\r{msg}{f" (x{self._repeat_counter})" if self._repeat_counter > 1 else ""}", end="")
File without changes
File without changes