Key Concepts
This chapter covers two key concepts essential to understanding and using this library:
Handling Different Robots – Learn how
voraus-robot-armhandles various robotsAsynchronous Instruction Execution – Learn about synchronous and asynchronous operations
Handling Different Robots
Robot arms are capable of performing a wide range of tasks, though not all robots are equally suited for every application. While the functionality and behavior of robots may vary, many fundamental actions — such as reading joint states or controlling joint movements within joint space — often share common characteristics across different robotic systems.
To streamline development and enhance usability, this library aims to standardize these shared functionalities while also clearly distinguish where robots differ in capabilities.
Modular Robot Definition
Within the context of this library, a robot is defined in terms of its features and characteristics in a modular fashion, allowing functionality to be shared across multiple robot platforms. A robot is therefore represented as a pool of actions it can perform. For example:
Robot A:
MovePTP
Robot B:
MovePTP
MoveLinear
MoveToContact
In this case, both Robot A and Robot B share the MovePTP function, which operates identically across the two robots. Consequently, for applications that only require the MovePTP action, the choice of robot becomes interchangeable. If the application requires the MoveToContact functionality, only a robot with this capability can be used (in this case only Robot B).
All available functionalities are made accessible to users via type-hints and autocompletion, ensuring that users can quickly identify which capabilities are supported by a given robot.
This design contributes to a user experience that adheres to the “Principle of Least Astonishment” (POLA), especially when interacting with robots from different manufacturers.
Contract-Based Traits
Technically, the modular robot definition is achieved by employing a contract-based trait system. A trait is a set of abstract methods that define the expected behaviors of a robot, ensuring consistency across different implementations. This approach is analogous to concepts in other programming languages, such as:
Interfaces in Java: Learn more
Traits in Rust: Learn more
Abstract Classes in C++: Learn more
Protocols in Python: Learn more
Robot Behavior Groups
voraus-robot-arm allows porting your applications to different robot types while
also not limiting the usable feature set to the most common ground. As such, it allows to select the amount of
portability of your application by opting in to use special robot behavior groups.
Most robots implement portable general purpose traits, which other robots will support as well. Method
and trait names do not contain special identifiers or names. One example is the MovePTPTrait with its method move_ptp().
However, these portable traits might not expose all functionality that the specific robot in use provides.
To still be able to utilize all functionalities a robot supports, special behavior groups of robots are defined.
Those groups are named via simple NATO alphabet identifiers to distinguish them from their generic counterparts.
As a result, a robot that supports a Victor behavior may additionally provide the MovePTPVictorTrait
with its method move_ptp_v().
In order to enable portability of your application, it is recommended to stick to methods without an identifier whenever possible. Only use special robot behavior methods if you need behavior that is not covered by a generic trait.
Attention
Whenever you see a letter appended to a method for example move_linear_v(), it means that this method implements
robot specific behavior and thus might not be easily portable.
Type-Hinting and Development Support
The library leverages type-hints to improve the user experience by enabling full editor support, including autocompletion and autosuggestions. This also allows for integration with type-checking tools such as mypy, further enhancing development workflows (CI/CD) and reducing bugs.
Example
In order to yield a seamless experience with all robots that are supported by voraus robotik,
voraus-robot-arm provides ready-to-use robots that the user can import.
As explained in the previous section, a robot is a just a combination of trait implementations.
Therefore, the definition, implementation, manipulation and extension of robots is highly flexible.
For example, it is possible to just import and use one of the provided robots:
from voraus_robot_arm import VorausIndustrialRobotArm
and use it as type-hint through the entire application.
def my_application_specific_robot(
robot: VorausIndustrialRobotArm,
) -> None:
"""Can only be executed by a specific robot.
Special behavior from Robot Behavior Group Victor is used although not
necessary.
"""
robot.move_ptp_v(HOME).result()
my_utility(robot)
# The generic move_ptp is also available.
robot.move_ptp(UP).result()
The benefit of this approach is mainly the low entry barrier, because all functionality of the chosen robot is exposed via autosuggestion and can be used directly. The downside is that the application might be bound to the chosen robot because robot specific functionality was used (although a generic alternative was available).
The strongly recommended alternative is, to only type-hint which functionality is actually needed and chose the most generic one that still fulfills the requirements. By doing so, the created application only requires a robot which satisfies the constraints and not a specific robot. As a result the robot can be changed later without touching the application code.
To type-hint the overall application function, it is most convenient to define the required traits like so:
class _RequiredRobotTraits(MovePTPTrait, MoveLinearTrait, Protocol): ...
and use them like this:
def my_application_generic_robot(robot: _RequiredRobotTraits) -> None:
"""Can be executed by different robots.
This application can be executed with any robot that supports
the required traits. Only generic traits (no Robot Behavior Group) are
used, resulting in a portable and reusable application.
"""
robot.move_ptp(HOME).result()
my_utility(robot)
robot.move_ptp(UP).result()
The advantage here is that only the functionality which is actually used is required from the given robot, which means the application works with any robot that supports those traits. Furthermore, the autosuggestion will only provide methods that are actually available.
It is also possible to further narrow down the type-hint for certain utility functions like so:
def my_utility(robot: MoveLinearTrait) -> None:
"""A generic utility function.
This function only needs a robot that supports the MoveLinearTrait.
"""
robot.move_linear_relative(CartesianPose(x=-0.2)).result()
Resulting in a utility function that is reusable in every scenario where a robot that implements the MoveLinearTrait
is used.
The full example is provided below.
Quick Start Example
"""Example for working with traits."""
# In the next line ruff is instructed to not organize the imports
# for better readability in the documentation.
from math import pi # noqa: I001
from typing import Protocol
from voraus_robot_arm import VorausIndustrialRobotArm
from voraus_robot_arm import (
FanucRobotArm,
CartesianPose,
MoveLinearTrait,
MovePTPTrait,
)
VORAUS_CORE_HOST_1 = "localhost"
VORAUS_ROBOT_CONTROL_PORT_1 = 48401
VORAUS_CORE_HOST_2 = "localhost"
VORAUS_ROBOT_CONTROL_PORT_2 = 58401
HOME = CartesianPose(x=0.4, y=0.0, z=0.6, rx=-pi, ry=0, rz=pi / 2)
UP = CartesianPose(x=0.4, y=0.0, z=0.8, rx=-pi, ry=0, rz=pi / 2)
class _RequiredRobotTraits(MovePTPTrait, MoveLinearTrait, Protocol): ...
def my_application_specific_robot(
robot: VorausIndustrialRobotArm,
) -> None:
"""Can only be executed by a specific robot.
Special behavior from Robot Behavior Group Victor is used although not
necessary.
"""
robot.move_ptp_v(HOME).result()
my_utility(robot)
# The generic move_ptp is also available.
robot.move_ptp(UP).result()
def my_application_generic_robot(robot: _RequiredRobotTraits) -> None:
"""Can be executed by different robots.
This application can be executed with any robot that supports
the required traits. Only generic traits (no Robot Behavior Group) are
used, resulting in a portable and reusable application.
"""
robot.move_ptp(HOME).result()
my_utility(robot)
robot.move_ptp(UP).result()
def my_utility(robot: MoveLinearTrait) -> None:
"""A generic utility function.
This function only needs a robot that supports the MoveLinearTrait.
"""
robot.move_linear_relative(CartesianPose(x=-0.2)).result()
if __name__ == "__main__":
my_first_robot = VorausIndustrialRobotArm()
my_second_robot = FanucRobotArm()
with (
my_first_robot.connect(
VORAUS_CORE_HOST_1, VORAUS_ROBOT_CONTROL_PORT_1
),
my_second_robot.connect(
VORAUS_CORE_HOST_2, VORAUS_ROBOT_CONTROL_PORT_2
),
):
my_first_robot.enable()
my_second_robot.enable()
my_application_specific_robot(my_first_robot)
my_application_generic_robot(my_first_robot)
my_application_generic_robot(my_second_robot)
Asynchronous Instruction Execution
The voraus-robot-arm Python package provides synchronous operations called commands and asynchronous operations
called instructions.
A command is an operation that is executed immediately and is therefore synchronous by default. Example commands include:
Set time override
Robot Stop
Robot Pause/Continue
An instruction is a robot operation that is queued and then sent to the robot. An instruction may take time to execute if other instructions are ahead in the queue, e.g. multiple motion instructions. Therefore, instructions are processed asynchronously by default. Example instructions include:
Move PTP
Move Linear
Move Circular
Wait Time
Commands are executed immediately and have no return values. Instructions are asynchronous, thus they return an object
to track their progress called a Future.
Working with Futures
A future is a handle to track the progress of an instruction. The call to the instruction itself does
not block, instead, the call returns a Future object and the Python interpreter continues immediately.
vertical: Future = robot.move_ptp(VERTICAL)
_logger.info("Instruction has been sent")
In this example, while the robot moves to the vertical pose, the Python interpreter continues its execution.
Instruction has been sent is printed before the robot reaches the vertical pose.
At any time in your application, it is possible to wait for the completion of one instruction by calling the
result() method of the Future. It will block, until the instruction the Future belongs to has a result.
A result can either be the completion of the instruction - most of the time the target pose is reached - or an error,
that caused the instruction to fail.
vertical: Future = robot.move_ptp(VERTICAL)
_logger.info("Instruction has been sent")
vertical.result()
_logger.info("Vertical reached")
With the extension above, the Python interpreter waits for the robot to reach the VERTICAL pose before continuing
with the logging statement.
In case you want to synchronize the Python interpreter with the robot motion, simply append
a result() to each instruction method that is called. The Python interpreter will only continue with the
next instruction, once the previous instruction has finished.
robot.move_ptp(HOME).result()
_logger.info("Home reached")
robot.move_ptp(VERTICAL).result()
_logger.info("Vertical reached")
If you need to execute a block of instructions, e.g. because the motion segments shall be blended, but want to wait
for the whole section to complete, simply wait for the last Future.
robot.move_ptp(HOME_C)
robot.move_ptp(BLENDING_1, blending=Percent(50))
robot.move_ptp(BLENDING_2, blending=Percent(50))
blending_3: Future = robot.move_ptp(BLENDING_3)
_logger.info("Sent instructions")
blending_3.result()
_logger.info("BLENDING_3 reached")
A note with regards to blending: Blending only works, if the subsequent instruction used for blending is known to the robot control in advance. As such, it is not possible to blend two instructions, which are synced to the Python interpreter.
robot.move_ptp(HOME_C)
robot.move_ptp(BLENDING_1, blending=Percent(50)).result()
robot.move_ptp(BLENDING_2) # never reached
Instead queue the instructions asynchronously.
robot.move_ptp(HOME_C)
robot.move_ptp(BLENDING_1, blending=Percent(50))
robot.move_ptp(BLENDING_2).result()
_logger.info("BLENDING_2 reached")
Full Example of Asynchronous Execution
Asynchronous Instruction Execution example
"""Examples how to execute asynchronous instructions."""
from logging import Logger, getLogger
from math import radians
from typing import Protocol, runtime_checkable
from voraus_robot_arm import (
CartesianPose,
Future,
JointPose,
MovePTPTrait,
Percent,
VorausIndustrialRobotArm,
configure_logging,
x,
y,
z,
)
_logger: Logger = getLogger(__name__)
VORAUS_CORE_HOST = "localhost"
VORAUS_CORE_PORT = 48401
HOME = JointPose().from_list(
[radians(d) for d in [0, -90, 90, -90, -90, 0]]
)
VERTICAL = JointPose().from_list(
[radians(d) for d in [0, -90, 0, -90, -90, 0]]
)
HOME_C = CartesianPose().from_list(
[0.550, -0.1382, 0.4743, 3.14, -0, 1.57]
)
BLENDING_1 = HOME_C - z(0.2)
BLENDING_2 = BLENDING_1 + y(0.2)
BLENDING_3 = BLENDING_2 - x(0.2)
@runtime_checkable
class _RequiredRobotTraits(MovePTPTrait, Protocol): ...
def run_instructions_example(robot: _RequiredRobotTraits) -> None:
"""Simple Instructions example."""
vertical: Future = robot.move_ptp(VERTICAL)
_logger.info("Instruction has been sent")
vertical.result()
_logger.info("Vertical reached")
def run_synchronous_example(robot: _RequiredRobotTraits) -> None:
"""Example of synchronous instructions."""
robot.move_ptp(HOME).result()
_logger.info("Home reached")
robot.move_ptp(VERTICAL).result()
_logger.info("Vertical reached")
def run_block_example(robot: _RequiredRobotTraits) -> None:
"""Example of a block of instructions."""
robot.move_ptp(HOME_C)
robot.move_ptp(BLENDING_1, blending=Percent(50))
robot.move_ptp(BLENDING_2, blending=Percent(50))
blending_3: Future = robot.move_ptp(BLENDING_3)
_logger.info("Sent instructions")
blending_3.result()
_logger.info("BLENDING_3 reached")
def blending_sync_not_working(robot: _RequiredRobotTraits) -> None:
"""Example of blending not working with synchronous instructions."""
robot.move_ptp(HOME_C)
robot.move_ptp(BLENDING_1, blending=Percent(50)).result()
robot.move_ptp(BLENDING_2) # never reached
def run_blending_sync_example(robot: _RequiredRobotTraits) -> None:
"""Example of blending working with synchronous instructions."""
robot.move_ptp(HOME_C)
robot.move_ptp(BLENDING_1, blending=Percent(50))
robot.move_ptp(BLENDING_2).result()
_logger.info("BLENDING_2 reached")
if __name__ == "__main__":
configure_logging()
robot = VorausIndustrialRobotArm()
with robot.connect(host=VORAUS_CORE_HOST, port=VORAUS_CORE_PORT):
robot.enable()
robot.move_ptp(HOME).result()
run_instructions_example(robot)
run_synchronous_example(robot)
run_block_example(robot)
run_blending_sync_example(robot)