|
| 1 | +"""Bindings for Sensapex uMp-3 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 ( |
| 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 | +) |
| 20 | + |
| 21 | + |
| 22 | +class Ump3Bindings(BaseBindings): |
| 23 | + """Bindings for UMP-3 platform""" |
| 24 | + |
| 25 | + def __init__(self) -> None: |
| 26 | + """Initialize UMP-3 bindings.""" |
| 27 | + |
| 28 | + # Establish connection to Sensapex API (exit if connection fails). |
| 29 | + UMP.set_library_path(RESOURCES_PATH) |
| 30 | + self._ump = UMP.get_ump() |
| 31 | + if self._ump is None: |
| 32 | + error_message = "Unable to connect to uMp" |
| 33 | + raise ValueError(error_message) |
| 34 | + |
| 35 | + async def get_manipulators(self) -> list[str]: |
| 36 | + return list(map(str, self._ump.list_devices())) |
| 37 | + |
| 38 | + async def get_axes_count(self) -> int: |
| 39 | + return 3 |
| 40 | + |
| 41 | + def get_dimensions(self) -> Vector4: |
| 42 | + return Vector4(x=20, y=20, z=20, w=20) |
| 43 | + |
| 44 | + async def get_position(self, manipulator_id: str) -> Vector4: |
| 45 | + manipulator_position = self._get_device(manipulator_id).get_pos(1) |
| 46 | + |
| 47 | + # Add the depth axis to the end of the position. |
| 48 | + manipulator_position.append(manipulator_position[0]) |
| 49 | + |
| 50 | + # Convert and return. |
| 51 | + return um_to_mm(array_to_vector4(manipulator_position)) |
| 52 | + |
| 53 | + # noinspection PyTypeChecker |
| 54 | + async def get_angles(self, _: str) -> Vector3: |
| 55 | + """uMp-3 does not support getting angles so raise an error. |
| 56 | +
|
| 57 | + :raises: AttributeError |
| 58 | + """ |
| 59 | + error_message = "UMP-3 does not support getting angles" |
| 60 | + raise AttributeError(error_message) |
| 61 | + |
| 62 | + # noinspection PyTypeChecker |
| 63 | + async def get_shank_count(self, _: str) -> int: |
| 64 | + """uMp-3 does not support getting shank count so raise an error. |
| 65 | +
|
| 66 | + :raises: AttributeError |
| 67 | + """ |
| 68 | + error_message = "UMP-3 does not support getting shank count" |
| 69 | + raise AttributeError(error_message) |
| 70 | + |
| 71 | + def get_movement_tolerance(self) -> float: |
| 72 | + return 0.001 |
| 73 | + |
| 74 | + # noinspection DuplicatedCode |
| 75 | + async def set_position(self, manipulator_id: str, position: Vector4, speed: float) -> Vector4: |
| 76 | + # Convert position to micrometers. |
| 77 | + target_position_um = vector_mm_to_um(position) |
| 78 | + |
| 79 | + # Request movement. |
| 80 | + movement = self._get_device(manipulator_id).goto_pos( |
| 81 | + vector4_to_array(target_position_um), scalar_mm_to_um(speed) |
| 82 | + ) |
| 83 | + |
| 84 | + # Wait for movement to complete. |
| 85 | + await get_running_loop().run_in_executor(None, movement.finished_event.wait, None) |
| 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 set_depth(self, manipulator_id: str, depth: float, speed: float) -> float: |
| 95 | + # Augment current position with depth. |
| 96 | + current_position = await self.get_position(manipulator_id) |
| 97 | + new_platform_position = current_position.model_copy(update={"x": depth}) |
| 98 | + |
| 99 | + # Make the movement. |
| 100 | + final_platform_position = await self.set_position(manipulator_id, new_platform_position, speed) |
| 101 | + |
| 102 | + # Return the final depth. |
| 103 | + return float(final_platform_position.w) |
| 104 | + |
| 105 | + async def stop(self, manipulator_id: str) -> None: |
| 106 | + self._get_device(manipulator_id).stop() |
| 107 | + |
| 108 | + def platform_space_to_unified_space(self, platform_space: Vector4) -> Vector4: |
| 109 | + # unified <- platform |
| 110 | + # +x <- +y |
| 111 | + # +y <- -x |
| 112 | + # +z <- -z |
| 113 | + # +d <- +d/x |
| 114 | + |
| 115 | + return Vector4( |
| 116 | + x=platform_space.y, |
| 117 | + y=self.get_dimensions().x - platform_space.x, |
| 118 | + z=self.get_dimensions().z - platform_space.z, |
| 119 | + w=platform_space.w, |
| 120 | + ) |
| 121 | + |
| 122 | + def unified_space_to_platform_space(self, unified_space: Vector4) -> Vector4: |
| 123 | + # platform <- unified |
| 124 | + # +x <- -y |
| 125 | + # +y <- +x |
| 126 | + # +z <- -z |
| 127 | + # +d/x <- +d |
| 128 | + |
| 129 | + return Vector4( |
| 130 | + x=self.get_dimensions().y - unified_space.y, |
| 131 | + y=unified_space.x, |
| 132 | + z=self.get_dimensions().z - unified_space.z, |
| 133 | + w=unified_space.w, |
| 134 | + ) |
| 135 | + |
| 136 | + # Helper methods. |
| 137 | + def _get_device(self, manipulator_id: str) -> SensapexDevice: |
| 138 | + return self._ump.get_device(int(manipulator_id)) |
0 commit comments