"""
"""
from __future__ import annotations
import time
from typing import Optional, List, Tuple, Dict, Callable
from abc import ABC, abstractmethod
from pathlib import Path
from genericroboticarm.control.device_interaction import DeviceList
from genericroboticarm.control.graph_manager import GraphManager, JointState
from genericroboticarm.control.manual_movement import ManualMovementManager
from sila2.client import ClientObservableCommandInstance
from sila2.framework import CommandExecutionStatus
from threading import Lock
import logging
from datetime import datetime
[docs]
def finish_successfully(cmd_infos: list[ClientObservableCommandInstance]) -> bool:
while not all(cmd_info.done for cmd_info in cmd_infos):
time.sleep(.05)
success = all(cmd_info.status == CommandExecutionStatus.finishedSuccessfully for cmd_info in cmd_infos)
return success
[docs]
class RoboInterface(ABC):
current_position: Optional[str] = None
next_positions: List[str]
graph_dir: Path | None = None
def __init__(self, start_simulating: bool = True, **kwargs):
self.is_in_simulation_mode = start_simulating
self.precision = .3 # 1 is normal, higher means more intermediate steps of movements
self.speed = 100 # in percent
self.halt = False
self.next_positions = []
self.graph_manager = GraphManager(name=self.name, custom_dir=self.graph_dir)
self.manual_mover = ManualMovementManager(self, joint_names=self.joint_names)
self.move_lock = Lock()
# unlike move_lock, this is held for a whole robot task instead of a single movement.
# A command may hand it over to a deferred tail task, see Server.robot_command().
self.task_lock = Lock()
self._deferred_tail: Callable[[], None] | None = None
self._deferred_failure: BaseException | None = None
self.interacting_devices = DeviceList(self.name)
[docs]
def defer(self, tail_task: Callable[[], None]) -> None:
"""
Registers work that may be executed after the running command has reported success,
e.g. storing a lid away once the robot is clear of the device it just served.
The robot stays reserved until the tail task is done, so the next command has to wait
for it, but the client and the served device are released early.
Only one tail task can be pending at a time.
:param tail_task: the work to be done once the current command returned
:return:
"""
if self._deferred_tail is not None:
raise RuntimeError("There is already a deferred tail task registered")
self._deferred_tail = tail_task
[docs]
def pop_deferred(self) -> Callable[[], None] | None:
"""
Returns the registered tail task, if any, and forgets about it.
:return: the tail task or None
"""
tail_task, self._deferred_tail = self._deferred_tail, None
return tail_task
[docs]
def report_deferred_failure(self, error: BaseException):
"""
Remembers that a tail task failed. Since its command has already reported success,
the failure can only be handed to the next command touching the robot.
:param error:
:return:
"""
self._deferred_failure = error
[docs]
def take_deferred_failure(self) -> BaseException | None:
"""
Returns the failure of the last tail task, if any, and forgets about it.
:return: the error or None
"""
error, self._deferred_failure = self._deferred_failure, None
return error
@property
def currently_gripping_plate(self) -> bool:
"""
Whether the gripper currently holds a plate. Robots that cannot sense their gripper
report False, so the guards against picking with an occupied gripper do not fire for
them. Subclasses that can tell override this.
:return:
"""
return False
@property
def name(self)-> str:
return self.get_name()
[docs]
@classmethod
@abstractmethod
def get_name(cls) -> str:
"""
The name will be used as parameter in command line startup (-r),
as sila server name and to save the position graph.
:return: the name
"""
[docs]
@classmethod
@abstractmethod
def get_used_cli_args(cls) -> dict[str, str]:
"""
Defines the arguments that are forwarded from the command line to the robots constructor.
:return: a dictionary of type [constructor parameter name, command line argument name]
"""
[docs]
@abstractmethod
def init_connection(self, **kwargs):
"""
Sets up the hardware connection
:return:
"""
@property
@abstractmethod
def joint_names(self) -> list[str]:
"""
Returns a list of names of the individual coordinates used by the robot.
For example [x, y, z, roll, pitch, grip] or [j1, j2, j3, j4, j5]
:return:
"""
[docs]
def set_metrik(self, metrik: Callable[[JointState, JointState], float]):
"""
Sets the way
:param metrik:
:return:
"""
self.graph_manager.dist = metrik
self.graph_manager.update_distances()
[docs]
def stop_moving(self):
"""
Immediately(Or as quickly as possible) stops all movements
:return:
"""
print(f"{datetime.now()}: Immediately stopped moving")
self.next_positions = []
# wait for movement command to finish and nullify the current position (we do not know where we stopped)
with self.move_lock:
self.current_position = None
[docs]
def set_speed(self, new_speed: float):
"""
Set the speed
:param new_speed:
:return:
"""
self.speed = new_speed
[docs]
def set_acceleration(self, new_acceleration: float, **kwargs):
"""
:param new_acceleration:
:return:
"""
[docs]
def move_to_nearest_known_position(self, offset: Optional[Dict[str, float]]=None):
"""
:return:
"""
nearest = self.graph_manager.get_nearest_position(self.current_coordinates)
self.move_straight_to_position(nearest, offset=offset)
self.current_position = nearest
[docs]
@abstractmethod
def move_relative(self, movements: List[Tuple[str, float]], **kwargs):
"""
:param movements:
:return:
"""
[docs]
def move_to_position(self, target_position: str, offset: Optional[Dict[str, float]]=None):
"""
:param offset:
:param target_position:
:return:
"""
logging.info(f"{datetime.now()}: moving to {target_position}.")
# check whether the position exists
if not self.graph_manager.position_known(target_position):
logging.error(f"{datetime.now()}: Position {target_position} is unknown.")
raise ValueError(f"Position {target_position} is unknown.")
with self.move_lock:
# move to nearest known position if not currently at a known position
if not self.current_position:
start = self.graph_manager.get_nearest_position(self.current_coordinates)
self.next_positions = [start]
else:
start = self.current_position
self.next_positions = []
# find the shortest path to target
path = self.graph_manager.get_shortest_path(start, target_position)
# add the path to next_positions or move directly to the target if no path exists
if path:
self.next_positions.extend(path)
else:
self.next_positions.append(target_position)
# move to target
while self.next_positions:
self.move_straight_to_position(self.next_positions[0], offset=offset)
self.current_position = self.next_positions[0]
self.next_positions.pop(0)
[docs]
@abstractmethod
def grip_close(self, **kwargs):
"""
:return:
"""
[docs]
@abstractmethod
def grip_open(self, **kwargs):
"""
:return:
"""
[docs]
def pick_from_device(self, device: str, pos: int, offset: Optional[Dict[str, float]]=None):
"""
:param device:
:param pos:
:param offset:
:return:
"""
source_interactive = device in self.interacting_devices.devices
cmd_to_finish = []
if source_interactive:
source_client = self.interacting_devices.get_client(device)
if not source_client:
raise ConnectionError(f"Could not connect to source device {device}")
cmd_to_finish.append(source_client.LabwareTransferSiteController.PrepareForOutput((device, 1), pos+1))
# do this while the source device is preparing as well
if hasattr(self, "prepare_for_input"):
self.prepare_for_input(0, device, pos+1, "plate_type")
if not finish_successfully(cmd_to_finish):
logging.error(f"{datetime.now()}: Preparations for labware transfer failed")
if hasattr(self, "cancel_transfer"):
self.cancel_transfer()
raise Exception("Preparations for labware transfer failed")
position_identifier = self.site_to_position_identifier(device, pos)
self.pick_at_position(position_identifier, offset)
if source_interactive:
source_client.LabwareTransferSiteController.LabwareRemoved((device, pos+1))
[docs]
def pick_at_position(self, identifier: str, offset: Optional[Dict[str, float]]=None):
"""
Opens the gripper, moves to a specified location and closes the gripper there
:param identifier: Position identifier
:param offset:
:return:
"""
if not self.graph_manager.position_known(identifier):
raise ValueError(f"Position {identifier} is unknown.")
self.grip_open()
self.move_to_position(identifier, offset=offset)
self.grip_close()
[docs]
def place_at_device(self, device: str, pos: int, offset: Optional[Dict[str, float]]=None,
plate_type: str = "plate_type"):
"""
:param plate_type:
:param device:
:param pos:
:param offset:
:return:
"""
target_interactive = device in self.interacting_devices.devices
cmd_to_finish = []
if target_interactive:
target_client = self.interacting_devices.get_client(device)
if not target_client:
raise ConnectionError(f"Could not connect to target device {device}")
cmd_to_finish.append(target_client.LabwareTransferSiteController.PrepareForInput(
(device, 1), pos+1, plate_type, "uuid"))
print(f"{datetime.now()}: calling for preparation for input {(device, 1), pos+1, plate_type}")
# do this while the target device is preparing as well
if hasattr(self, "prepare_for_output"):
self.prepare_for_output(0, device, pos+1)
if not finish_successfully(cmd_to_finish):
logging.error(f"{datetime.now()}: Preparations for labware transfer failed")
if hasattr(self, "cancel_transfer"):
self.cancel_transfer()
raise Exception("Preparations for labware transfer failed")
position_identifier = self.site_to_position_identifier(device, pos)
self.place_at_position(position_identifier, offset)
if target_interactive:
target_client.LabwareTransferSiteController.LabwareDelivered((device, pos+1))
[docs]
def place_at_position(self, identifier: str, offset: Optional[Dict[str, float]]=None):
"""
Opens the gripper, moves to a specified location and opens the gripper there
:param identifier: Position identifier
:param offset:
:return:
"""
if not self.graph_manager.position_known(identifier):
logging.error(f"{datetime.now()}: Position {identifier} is unknown.")
return
self.move_to_position(identifier, offset=offset)
self.grip_open()
[docs]
def site_to_position_identifier(self, device: str, slot: int) -> str:
logging.warning(f"{datetime.now()}: Using the default translation of simply concatenating device_name and slot number")
return f"{device}{slot}"
[docs]
def move_straight_to_position(self, identifier: str, offset: Optional[Dict[str, float]]=None):
"""
Moves the robot straight to the specified position, i.e. queries its coordinates, adds the offset and call
move_to_coordinates() on the result.
:param offset:
:param identifier:
:return:
"""
if identifier not in self.graph_manager.g.nodes:
logging.warning(f"{datetime.now()}: Unknown position: {identifier}")
if not offset:
offset = dict()
if not self.halt:
target_coord = self.graph_manager.get_coordinates(identifier)
for joint, diff in offset.items():
if joint not in target_coord:
logging.warning(f"{datetime.now()}: {joint} is an unknown joint name in offset")
continue
target_coord[joint] += diff
kwargs = {}
if self.current_position:
kwargs = self.graph_manager.get_edge_attributes(self.current_position, identifier)
self.move_to_coordinates(target_coord, **kwargs)
[docs]
@abstractmethod
def move_to_coordinates(self, coords: JointState, **kwargs) -> None:
"""
:param coords:
:return:
"""
@property
@abstractmethod
def current_coordinates(self) -> JointState:
"""
:return:
"""
[docs]
def end_connection(self, **kwargs):
"""
Is executed when the server stops
:return:
"""
[docs]
def run_custom_command(self, cmd: str) -> str:
"""
Runs a custom command on the hardware. The implementation for a specific hardware might look very different
and the response must be a string. Be default returns 'Not Implemented'. Use on your own risk.
:param cmd:
:return:
"""
return "Not Implemented"