Skip to content

Commit

Permalink
Fleet adapter and PerformAction tutorial (#105)
Browse files Browse the repository at this point in the history
* Add EFC writeup

Signed-off-by: Xi Yu Oh <[email protected]>

* Cleanup tutorial page

Signed-off-by: Xi Yu Oh <[email protected]>

* Add EFC tutorial

Signed-off-by: Xi Yu Oh <[email protected]>

* Remove max_delay

Signed-off-by: Xi Yu Oh <[email protected]>

* Update add robot loop

Signed-off-by: Xi Yu Oh <[email protected]>

* Update link

Signed-off-by: Xi Yu Oh <[email protected]>

* Easy API and PerformAction tutorials

Signed-off-by: Xiyu Oh <[email protected]>

* Use main branch in links

Signed-off-by: Xiyu Oh <[email protected]>

* Review comments

Signed-off-by: Xiyu Oh <[email protected]>

---------

Signed-off-by: Xi Yu Oh <[email protected]>
Signed-off-by: Xiyu Oh <[email protected]>
  • Loading branch information
xiyuoh committed Nov 9, 2023
1 parent 65a1f8a commit 0810513
Show file tree
Hide file tree
Showing 5 changed files with 590 additions and 344 deletions.
3 changes: 2 additions & 1 deletion src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
- [Integration](./integration.md)
- [Navigation Maps](./integration_nav-maps.md)
- [Mobile Robot Fleets](./integration_fleets.md)
- [Tutorials](./integration_fleets_tutorial.md)
- [Fleet Adapter Tutorial](./integration_fleets_adapter_tutorial.md)
- [PerformAction Tutorial](./integration_fleets_action_tutorial.md)
- [Free Fleet](./integration_free-fleet.md)
- [Read-Only Fleets](./integration_read-only.md)
- [Doors](./integration_doors.md)
Expand Down
4 changes: 3 additions & 1 deletion src/integration_fleets.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Mobile Robot Fleet Integration

Here we will cover integrating a mobile robot fleet that offers the **Full Control** category of fleet adapter, as discussed in the [RMF Core Overview](./rmf-core.md) chapter.
Here we will cover integrating a mobile robot fleet that offers the **Full Control** category of fleet adapter, as **discussed** in the [RMF Core Overview](./rmf-core.md) chapter.
This means we assume the mobile robot fleet manager allows us to specify explicit paths for the robot to follow, and that the path can be interrupted at any time and replaced with a new path.
Furthermore, each robot's position will be updated live as the robots are moving.

Expand All @@ -17,6 +17,8 @@ The C++ API for **Full Control** automated guided vehicle (AGV) fleets can be fo
* [`RobotUpdateHandle`](https://github.com/open-rmf/rmf_ros2/blob/main/rmf_fleet_adapter/include/rmf_fleet_adapter/agv/RobotUpdateHandle.hpp) - Use this to update the position of a robot and to notify the adapter if the robot's progress gets interrupted.
* [`RobotCommandHandle`](https://github.com/open-rmf/rmf_ros2/blob/main/rmf_fleet_adapter/include/rmf_fleet_adapter/agv/RobotCommandHandle.hpp) - This is a pure abstract interface class. The functions of this class must be implemented to call upon the API of the specific fleet manager that is being adapted.

The C++ API for **Easy Full Control** fleets provides a simple and more accessible way for users to integrate with the Full Control library without having to modify its internal logic. It can be found in the [rmf_fleet_adapter](https://github.com/open-rmf/rmf_ros2/tree/main/rmf_fleet_adapter) package of the `rmf_ros2` repo. The [`EasyFullControl`](https://github.com/open-rmf/rmf_ros2/blob/main/rmf_fleet_adapter/include/rmf_fleet_adapter/agv/EasyFullControl.hpp) class contains helpful methods for users to create a `Configuration` object from YAML files encapsulating important fleet configuration parameters and navigation graphs, as well as to make their own fleet adapter with the `Configuration` object. The `add_robot(~)` method is provided for users to add robots to the new fleet adapter. This method takes in various callbacks that should be written by the user, and will be triggered whenever RMF is retrieving robot state information from the fleet or sending out commands to perform a particular process (navigation, docking, action, etc.). An example of the EasyFullControl fleet adapter can be found in [`fleet_adapter.py`](https://github.com/open-rmf/rmf_demos/blob/main/rmf_demos_fleet_adapter/rmf_demos_fleet_adapter/fleet_adapter.py) under the `rmf_demos` repo.

The C++ API for **Traffic Light Control** fleets (i.e. fleets that only allow RMF to pause/resume each mobile robot) can also be found in the `rmf_fleet_adapter` package of the `rmf_ros2` repo. The API reuses the `Adapter` class and requires users to initialize their fleet using either of the APIs [here](https://github.com/open-rmf/rmf_ros2/blob/9b4b8a8cc38b323f875a55c70f307446584d1639/rmf_fleet_adapter/include/rmf_fleet_adapter/agv/Adapter.hpp#L106-L180). The user has the option to integrate via the [`TrafficLight`](https://github.com/open-rmf/rmf_ros2/blob/main/rmf_fleet_adapter/include/rmf_fleet_adapter/agv/TrafficLight.hpp) API or for greater convenience, via the [`EasyTrafficLight`](https://github.com/open-rmf/rmf_ros2/blob/main/rmf_fleet_adapter/include/rmf_fleet_adapter/agv/EasyTrafficLight.hpp) API.

The basic workflow of developing a fleet adapter is the following:
Expand Down
112 changes: 112 additions & 0 deletions src/integration_fleets_action_tutorial.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# PerformAction Tutorial (Python)

This tutorial is an extension of the Fleet Adapter Tutorial and will guide you to write custom actions in your fleet adapter. While RMF offers a few standard tasks, we understand that different robots may be equipped and programmed to perform different types of actions, such as cleaning, object-picking, teleoperation, and so on. By supporting custom tasks, users can trigger a custom action specified in the fleet adapter's `config.yaml` beforehand, and RMF would relinquish control of the robot until it is signalled that the robot has completed the custom action. You may explore the [Supporting a new Task in RMF](./task_new.md) section to read more about supporting custom tasks and how you can create your own task JSON to be sent to RMF.

In this tutorial, we will refer to a simplified version of the `rmf_demos_fleet_adapter` to implement a `Clean` PerformAction capability in our fleet adapter.

## 1. Define the PerformAction in the fleet `config.yaml`

We will need to define the name of the action in the fleet configuration, so that RMF recognizes this action as performable when a task is submitted and is able to dispatch it to a fleet that can fulfil it. In our `config.yaml` under the `rmf_fleet` section, we can provide a list of performable actions for our fleet. For example, let's define `clean` as an action supported by this fleet:

```yaml
rmf_fleet:
actions: ["clean"]
```
## 2. Apply action execution logic inside our fleet adapter
After RMF receives a task consisting of this action and dispatches it to the right fleet, the fleet adapter's `execute_action(~)` callback will be triggered. The `category` parsed to this callback corresponds to the action name that we have previously defined, and the `description` consists of any details about the action that we might be interested in.

Assume that this is the task JSON submitted to RMF:
```json
{
"type": "dispatch_task_request",
"request": {
"unix_millis_earliest_start_time": start_time,
"category": "clean",
"description": {
"zone": "clean_lobby"
}
}
}
```

In our example, the `category` provided would be `clean`, and the `description` would contain which cleaning zone this task is directing our robot to, which is `clean_lobby`. Hence, we will need to implement the logic in our `execute_action(~)`:

```python
def execute_action(self, category: str, description: dict, execution):
self.execution = execution
if category == 'clean':
self.perform_clean(description['zone'])
def perform_clean(self, zone):
if self.api.start_activity(self.name, 'clean', zone):
self.node.get_logger().info(
f'Commanding [{self.name}] to clean zone [{zone}]'
)
else:
self.node.get_logger().error(
f'Fleet manager for [{self.name}] does not know how to '
f'clean zone [{zone}]. We will terminate the activity.'
)
self.execution.finished()
self.execution = None
```

Since our fleet may be capable of performing multiple custom actions, we will need to conduct a check to ensure that the `category` received matches the robot API that we are targeting. Upon receiving a `clean` action, we can trigger the robot's API accordingly.

## 3. Implement the robot API for the custom action

This is where the `start_activity(~)` method inside `RobotClientAPI.py` comes into play. We would require it to implement the API call to the robot to start the cleaning activity. As an example, if the robot API uses REST to make calls to the robot, the implemented method may look like this:

```python
def start_activity(
self,
robot_name: str,
activity: str,
label: str
):
''' Request the robot to begin a process. This is specific to the robot
and the use case. For example, load/unload a cart for Deliverybot
or begin cleaning a zone for a cleaning robot.'''
url = (
self.prefix +
f"/open-rmf/rmf_demos_fm/start_activity?robot_name={robot_name}"
)
# data fields: task, map_name, destination{}, data{}
data = {'activity': activity, 'label': label}
try:
response = requests.post(url, timeout=self.timeout, json=data)
response.raise_for_status()
if self.debug:
print(f'Response: {response.json()}')
if response.json()['success']:
return True
# If we get a response with success=False, then
return False
except HTTPError as http_err:
print(f'HTTP error for {robot_name} in start_activity: {http_err}')
except Exception as err:
print(f'Other error {robot_name} in start_activity: {err}')
return False
```

## 4. Complete the action

Since we stored a `self.execution` object in our `RobotAdapter`, we will be notified when any execution (navigation, stop, or action) is completed as the update loop continually calls `is_command_completed` to check on its status.

```python
def update(self, state):
activity_identifier = None
if self.execution:
if self.api.is_command_completed():
self.execution.finished()
self.execution = None
else:
activity_identifier = self.execution.identifier
```

If your implementation requires a separate callback to mark the execution as finished, you can create a new function to conduct this check and call `self.execution.finished()` when the action is completed.
Loading

0 comments on commit 0810513

Please sign in to comment.