Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate mobile robots by interfacing with airo-tulip #147

Merged
merged 30 commits into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
5e39def
First implementation of KELORobile for airo-tulip v0.0.1
m-decoster Jul 15, 2024
6dcbbcd
Add wait() to robot testing
m-decoster Jul 15, 2024
952999e
Update airo-tulip to 0.0.3 for compliant mode
m-decoster Aug 14, 2024
a131a71
Compliant modes
m-decoster Aug 20, 2024
bb00fa1
Update airo-tulip to 0.0.5 (align drives)
m-decoster Aug 22, 2024
b012b19
Remove instantaneous argument that is no longer supported
m-decoster Aug 22, 2024
0d54423
airo-tulip -> 0.0.6
m-decoster Aug 22, 2024
4d7beaf
Use Enums for MobileRobot compliance levels instead of integers
m-decoster Aug 22, 2024
538e64a
Add get_odometry method for Robile
m-decoster Aug 26, 2024
853b0af
Zed coordinate system Z up X forward
m-decoster Aug 27, 2024
12bd218
Change Zed camera coordinate system
m-decoster Sep 2, 2024
90d7241
Format code
m-decoster Sep 3, 2024
827cb34
Add documentation
m-decoster Sep 3, 2024
d68447d
Higher frequency awaitable loop for KELORobile commands
m-decoster Sep 4, 2024
6273631
add position control for mobile robots
bitscuity Sep 5, 2024
d482fae
fix position control issues
bitscuity Sep 5, 2024
a9b0238
Restore manual_robot_testing
m-decoster Sep 6, 2024
083a8e7
Avoid setting negative timeouts
m-decoster Sep 17, 2024
276b6a5
Add reset_odometry function
m-decoster Sep 17, 2024
90a6432
Bump airo-tulip version for bug fixes
m-decoster Sep 19, 2024
81925b4
Add support for GetVelocity
m-decoster Sep 30, 2024
61d6b4c
Add airo-tulip to list of sister repositories
m-decoster Sep 30, 2024
359fbe5
Update changelog
m-decoster Oct 7, 2024
7c295db
Update dependency for airo-tulip to 0.0.10
m-decoster Oct 7, 2024
3d83198
Make MyPy happy
m-decoster Oct 7, 2024
56ce2ef
AwaitableAction control_loop returns a boolean, not None
m-decoster Oct 7, 2024
12a24c4
Force scalar bool from NumPy based on assumptions that norm returns a…
m-decoster Oct 7, 2024
8725cdc
Update to airo-tulip 0.1.0
m-decoster Oct 14, 2024
a0e94f5
Run position control loop in separate thread
m-decoster Oct 16, 2024
a303497
Add return type annotation -> None (mypy)
m-decoster Oct 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ This project uses a [CalVer](https://calver.org/) versioning scheme with monthly
- `Zed2i.ULTRA_DEPTH_MODE` to enable the ultra depth setting for the Zed2i cameras
- `OpenCVVideoCapture` implementation of `RGBCamera` for working with arbitrary cameras
- `MultiprocessRGBRerunLogger` and `MultiprocessRGBDRerunLogger` now allow you to pass an `entity_path` value which determines where the RGB and depth images will be logged
- `MobileRobot` and `KELORobile` interface and subclass added, to control mobile robots via the `airo-tulip` package

### Changed
- `coco-to-yolo` conversion now creates a single polygon of all disconnected parts of the mask instead of simply taking the first polygon of the list.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Repositories that follow the same style as `airo-mono` packages, but are not par
| 🛒 [`airo-models`](https://github.com/airo-ugent/airo-models) | Collection of robot and object models and URDFs |
| 🐉 [`airo-drake`](https://github.com/airo-ugent/airo-drake) | Integration with Drake |
| 🧭 [`airo-planner`](https://github.com/airo-ugent/airo-planner) | Motion planning interfaces |
| 🚗 [`airo-tulip`](https://github.com/airo-ugent/airo-tulip) | Driver for the KELO mobile robot platform |

### Usage & Philosophy 📖
We believe in *keep simple things simple*. Starting a new project should\* be as simple as:
Expand Down
Empty file.
Empty file.
119 changes: 119 additions & 0 deletions airo-robots/airo_robots/drives/hardware/kelo_robile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import math
import time

import numpy as np
from airo_robots.awaitable_action import AwaitableAction
from airo_robots.drives.mobile_robot import CompliantLevel, MobileRobot
from airo_tulip.api.client import KELORobile as KELORobileClient # type: ignore
from airo_tulip.hardware.platform_driver import PlatformDriverType # type: ignore
from airo_typing import Vector3DType


class KELORobile(MobileRobot):
"""KELO Robile platform implementation of the MobileRobot interface.

The KELO Robile platform consists of several drives with castor wheels which are connected via EtherCAT and can
be controlled individually. The airo-tulip API, which is used here, provides a higher level interface which
controls the entire platform."""

def __init__(self, robot_ip: str, robot_port: int = 49789):
"""Connect to the KELO robot.

The KELO robot should already be running the airo-tulip server. If this is not the case, any messages
sent from the client may be queued and executed once the server is started, resulting in unexpected
and/or sudden movements.

Args:
robot_ip: IP address of the KELO CPU brick.
robot_port: Port to connect on (default: 49789)."""
self._kelo_robile = KELORobileClient(robot_ip, robot_port)

def align_drives(self, x: float, y: float, a: float, timeout: float = 1.0) -> AwaitableAction:
"""Align all drives for driving in a direction given by the linear and angular velocities.

Beware that sending any other velocity commands before the awaitable is done may cause unexpected behaviour,
as this affects the "aligned" condition.

Args:
x: The x velocity (linear, m/s).
y: The y velocity (linear, m/s)
a: The angular velocity (rad/s).
timeout: The awaitable will finish after this time at the latest.

Returns:
An AwaitableAction which will check if the drives are aligned, or if the timeout has expired."""
self._kelo_robile.align_drives(x, y, a, timeout=timeout)

action_sent_time = time.time_ns()
return AwaitableAction(
lambda: self._kelo_robile.are_drives_aligned() or time.time_ns() - action_sent_time > timeout * 1e9,
default_timeout=2 * timeout,
default_sleep_resolution=0.002,
)

def set_platform_velocity_target(self, x: float, y: float, a: float, timeout: float) -> AwaitableAction:
self._kelo_robile.set_platform_velocity_target(x, y, a, timeout=timeout)

action_sent_time = time.time_ns()
return AwaitableAction(
lambda: time.time_ns() - action_sent_time > timeout * 1e9,
default_timeout=2 * timeout,
default_sleep_resolution=0.002,
)

def move_platform_to_pose(self, x: float, y: float, a: float, timeout: float) -> AwaitableAction:
target_pose = np.array([x, y, a])
action_start_time = time.time_ns()
action_timeout_time = action_start_time + timeout * 1e9

def control_loop() -> bool:
m-decoster marked this conversation as resolved.
Show resolved Hide resolved
current_pose = self._kelo_robile.get_odometry()
delta_pose = target_pose - current_pose

vel_vec_angle = np.arctan2(delta_pose[1], delta_pose[0]) - current_pose[2]
vel_vec_norm = min(np.linalg.norm(delta_pose[:2]), 0.5)
vel_x = vel_vec_norm * np.cos(vel_vec_angle)
vel_y = vel_vec_norm * np.sin(vel_vec_angle)

delta_angle = np.arctan2(np.sin(delta_pose[2]), np.cos(delta_pose[2]))
vel_a = max(min(delta_angle, math.pi / 4), -math.pi / 4)

command_timeout = (action_timeout_time - time.time_ns()) * 1e-9
if command_timeout >= 0.0:
self._kelo_robile.set_platform_velocity_target(vel_x, vel_y, vel_a, timeout=command_timeout)

at_target_pose = bool(np.linalg.norm(delta_pose) < 0.01)
stop = at_target_pose or time.time_ns() - action_start_time > timeout * 1e9

if stop:
self._kelo_robile.set_platform_velocity_target(0.0, 0.0, 0.0)

return stop

return AwaitableAction(
control_loop,
default_timeout=2 * timeout,
default_sleep_resolution=0.002,
)

def enable_compliant_mode(
self, enabled: bool, compliant_level: CompliantLevel = CompliantLevel.COMPLIANT_WEAK
) -> None:
if enabled:
if compliant_level == CompliantLevel.COMPLIANT_WEAK:
self._kelo_robile.set_driver_type(PlatformDriverType.COMPLIANT_WEAK)
elif compliant_level == CompliantLevel.COMPLIANT_MODERATE:
self._kelo_robile.set_driver_type(PlatformDriverType.COMPLIANT_MODERATE)
else:
self._kelo_robile.set_driver_type(PlatformDriverType.COMPLIANT_STRONG)
else:
self._kelo_robile.set_driver_type(PlatformDriverType.VELOCITY)

def get_odometry(self) -> Vector3DType:
return self._kelo_robile.get_odometry()

def get_velocity(self) -> Vector3DType:
return self._kelo_robile.get_velocity()

def reset_odometry(self) -> None:
self._kelo_robile.reset_odometry()
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
For information on how to set up the KELO Robile, please refer to the documentation of [airo-tulip](https://pypi.org/project/airo-tulip/).
31 changes: 31 additions & 0 deletions airo-robots/airo_robots/drives/hardware/manual_robot_testing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""code for manual testing of mobile robot base class implementations.
"""
from airo_robots.drives.hardware.kelo_robile import KELORobile
from airo_robots.drives.mobile_robot import MobileRobot


def manually_test_robot_implementation(robot: MobileRobot) -> None:
if isinstance(robot, KELORobile):
input("robot will rotate drives")
robot.align_drives(0.1, 0.0, 0.0, 1.0).wait()

input("robot will now move forward 10cm")
robot.set_platform_velocity_target(0.1, 0.0, 0.0, 1.0).wait()

input("robot will now move left 10cm")
robot.set_platform_velocity_target(0.0, 0.1, 0.0, 1.0).wait()

input("robot will now make two short rotations")
robot.set_platform_velocity_target(0.0, 0.0, 0.1, 1.0).wait()
robot.set_platform_velocity_target(0.0, 0.0, -0.1, 1.0).wait()

input("robot will now return to original position")
robot.set_platform_velocity_target(-0.1, -0.1, 0.0, 1.0).wait()

input("robot will now move to 10 cm along +X relative from its starting position with position control")
robot.move_platform_to_pose(0.1, 0.0, 0.0, 10.0).wait()


if __name__ == "__main__":
mobi = KELORobile("10.10.129.21")
manually_test_robot_implementation(mobi)
82 changes: 82 additions & 0 deletions airo-robots/airo_robots/drives/mobile_robot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from abc import ABC, abstractmethod
from enum import Enum

from airo_robots.awaitable_action import AwaitableAction
from airo_typing import Vector3DType


class CompliantLevel(Enum):
"""The level of compliance expected from the mobile robot.

Values may not correspond to identical behaviour on different mobile platforms, but are merely an indication.
A value of weak means a very compliant robot, whereas a value of strong means a slightly compliant robot."""

COMPLIANT_WEAK = 1
COMPLIANT_MODERATE = 2
COMPLIANT_STRONG = 3


class MobileRobot(ABC):
"""
Base class for a mobile robot.

Mobile robots typically allow to set a target velocity for the entire platform and apply torques to their wheels
to achieve the desired velocity.

All values are in metric units:
- Linear velocities are expressed in meters/second
- Angular velocities are expressed in radians/second
"""

@abstractmethod
def set_platform_velocity_target(self, x: float, y: float, a: float, timeout: float) -> AwaitableAction:
"""Set the desired platform velocity.

Args:
x: Linear velocity along the X axis.
y: Linear velocity along the Y axis.
a: Angular velocity.
timeout: After this time, the platform will automatically stop.

Returns:
An awaitable action."""

@abstractmethod
def move_platform_to_pose(self, x: float, y: float, a: float, timeout: float) -> AwaitableAction:
"""Move the platform to the given pose, without guarantees about the followed path.

Args:
x: Position along the startup pose's X axis.
y: Position along the startup pose's Y axis.
a: Orientation around the startup pose's Z axis.
timeout: After this time, the platform will automatically stop.

Returns:
An awaitable action."""

@abstractmethod
def enable_compliant_mode(self, enabled: bool, compliant_level: CompliantLevel) -> None:
"""Enable compliant mode on the robot.

Args:
enabled: If true, will enable compliant mode. Else, will disable compliant mode.
compliant_level: The level of compliance to be expected from the robot. Ignored if `enabled` is `False`."""

@abstractmethod
def get_odometry(self) -> Vector3DType:
"""Get the estimated robot pose as a 3D vector comprising the `x`, `y`, and `theta` values relative to the
robot's starting pose.

Returns: A 3D vector with a value for the `x` position, `y` position, and `theta` angle of the robot."""

@abstractmethod
def reset_odometry(self) -> None:
"""Reset the robot's odometry to `(0, 0, 0)`."""

@abstractmethod
def get_velocity(self) -> Vector3DType:
"""Get the estimated robot's velocity as a 3D vector comprising the `x`, `y` and `theta` values relative to the
robot's starting pose.

Returns:
A 3D vector with a value for the `x` velocity, `y` velocity, and `theta` angular velocity of the robot."""
1 change: 1 addition & 0 deletions airo-robots/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"click",
"airo-typing",
"airo-spatial-algebra",
"airo-tulip==0.1.0",
],
packages=setuptools.find_packages(),
package_data={"airo_robots": ["py.typed"]},
Expand Down
Loading