diff --git a/rmf_charging_schedule/CHANGELOG.rst b/rmf_charging_schedule/CHANGELOG.rst
new file mode 100644
index 000000000..bbd8f97d9
--- /dev/null
+++ b/rmf_charging_schedule/CHANGELOG.rst
@@ -0,0 +1,7 @@
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Changelog for package rmf_charging_schedule
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+2.3.2 (XXXX-YY-ZZ)
+------------------
+* First release
diff --git a/rmf_charging_schedule/README.md b/rmf_charging_schedule/README.md
new file mode 100644
index 000000000..3a14c818b
--- /dev/null
+++ b/rmf_charging_schedule/README.md
@@ -0,0 +1,39 @@
+A simple node that takes in a yaml file that describes a schedule for charger
+usage in an RMF scenario. The node will watch the clock and publish the
+appropriate commands to change the chargers of the robots.
+
+The format for the schedule looks like this:
+```
+"my_fleet_name":
+ "00:00": { "robot_1": "charger_A", "robot_2": "charger_B", "robot_3": "queue_A" }
+ "01:55": { "robot_1": "queue_B" }
+ "02:00": { "robot_3": "charger_A", "robot_1": "queue_A" }
+ "03:55": { "robot_2": "queue_B" }
+ "04:00": { "robot_1": "charger_B", "robot_2": "queue_A" }
+```
+
+The time format is `"HH:MM"` where `HH` ranges from `00` to `23` and `MM` ranges
+from `00` to `59`. Note that quotes are important because otherwise the yaml
+format may confuse the meaning of the colon `:`.
+
+The schedule will cycle every 24 hours.
+
+For each timestamp, only robots that are explicitly mentioned will have their
+dedicated charger changed. **It is the responsibility of the schedule file author
+to make sure that two robots are never assigned the same charger at the same
+time.** Failing to ensure this may cause traffic and task management to misbehave.
+
+When run in simulation mode (`--ros-args --use-sim-time`), the time `00:00` in
+the schedule will correspond to `t=0.0` in simulation time.
+
+When run without sim time on, the hours and minutes will correspond to the local
+timezone of the machine that the node is run on. To choose a specific timezone
+instead of using the system's local timzeone, use the `--timezone` argument and
+provide the desired [TZ identifier](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)
+string.
+
+It is advisable that you always put a `00:00` entry that indicates **all** of
+the intended charger assignments at midnight. When the node is launched, it will
+move through the schedule from the earliest entry up until the last relevant one
+and issue an initial charger assignment message based on what the assignments
+would have been if the schedule had run from `00:00`.
diff --git a/rmf_charging_schedule/package.xml b/rmf_charging_schedule/package.xml
new file mode 100644
index 000000000..f53610f8c
--- /dev/null
+++ b/rmf_charging_schedule/package.xml
@@ -0,0 +1,16 @@
+
+
+
+ rmf_charging_schedule
+ 2.3.2
+ Node for a fixed 24-hour rotating charger usage schedule
+ Grey
+ Apache License 2.0
+
+ rclpy
+ rmf_fleet_msgs
+
+
+ ament_python
+
+
diff --git a/rmf_charging_schedule/resource/rmf_charging_schedule b/rmf_charging_schedule/resource/rmf_charging_schedule
new file mode 100644
index 000000000..e69de29bb
diff --git a/rmf_charging_schedule/rmf_charging_schedule/__init__.py b/rmf_charging_schedule/rmf_charging_schedule/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/rmf_charging_schedule/rmf_charging_schedule/main.py b/rmf_charging_schedule/rmf_charging_schedule/main.py
new file mode 100644
index 000000000..9d90062c7
--- /dev/null
+++ b/rmf_charging_schedule/rmf_charging_schedule/main.py
@@ -0,0 +1,240 @@
+import sys
+import yaml
+import datetime
+import argparse
+from collections import OrderedDict
+import bisect
+from functools import total_ordering
+from zoneinfo import ZoneInfo
+
+from icecream import ic
+
+import rclpy
+from rclpy.node import Node, Publisher
+from rclpy.qos import (
+ QoSProfile, QoSHistoryPolicy, QoSReliabilityPolicy, QoSDurabilityPolicy
+)
+
+from rmf_fleet_msgs.msg import ChargingAssignment, ChargingAssignments
+
+@total_ordering
+class ScheduleTimePoint:
+ hour: int
+ minute: int
+
+ def __init__(self, hour: int, minute: int):
+ self.hour = hour
+ self.minute = minute
+
+ def parse(text: str):
+ segments = text.split(":")
+ assert len(segments) == 2, (
+ f'Time point text [{text}] does not have the correct HH:MM format'
+ )
+ hh = int(segments[0])
+ mm = int(segments[1])
+ assert 0 <= hh and hh < 24, (
+ f'Time point text [{text}] has an hour value which is outside the '
+ f'valid range of 0 -> 23.'
+ )
+ assert 0 <= mm and mm < 60, (
+ f'Time point text [{text}] has a minute value which is outside the '
+ f'valid range of 0 -> 59'
+ )
+ return ScheduleTimePoint(hh, mm)
+
+ def __eq__(self, other):
+ return self.hour == other.hour and self.minute == other.minute
+
+ def __lt__(self, other):
+ if self.hour < other.hour:
+ return True
+ elif self.hour > other.hour:
+ return False
+ return self.minute < other.minute
+
+ def __hash__(self):
+ return hash((self.hour, self.minute))
+
+
+class Assignment:
+ fleet: str
+ robot: str
+ charger: str
+
+ def __init__(self, fleet, robot, charger):
+ self.fleet = fleet
+ self.robot = robot
+ self.charger = charger
+
+
+def publish_assignments(publisher: Publisher, assignments: dict[dict[str]]):
+ for fleet, robots in assignments.items():
+ msg = ChargingAssignments()
+ msg.fleet_name = fleet
+ for robot, charger in robots.items():
+ assignment = ChargingAssignment()
+ assignment.robot_name = robot
+ assignment.waypoint_name = charger
+ # The mode isn't actually used yet, so it doesn't matter what we set
+ # it to.
+ assignment.mode = ChargingAssignment.MODE_CHARGE
+ msg.assignments.append(assignment)
+
+ publisher.publish(msg)
+
+def update_assignments(
+ last_update_index: int | None,
+ next_update_index: int,
+ sorted_times: list,
+ schedule: dict,
+ assignments: dict,
+ publisher: Publisher,
+ node: Node,
+):
+ for key in sorted_times[last_update_index:next_update_index]:
+ changes: list[Assignment] = schedule[key]
+ for change in changes:
+ assignments.setdefault(change.fleet, {})[change.robot] = change.charger
+ node.get_logger().info(
+ f'Sending {change.fleet}/{change.robot} to {change.charger} at '
+ f'{key.hour:02d}:{key.minute:02d}'
+ )
+ publish_assignments(publisher, assignments)
+
+
+def simulation_time(node: Node) -> ScheduleTimePoint:
+ seconds, _ = node.get_clock().now().seconds_nanoseconds()
+ minutes: int = int(seconds/60)
+ hour: int = int((minutes/60) % 24)
+ minute = minutes % 60
+ return ScheduleTimePoint(hour, minute)
+
+
+def real_time(node: Node, timezone: ZoneInfo) -> ScheduleTimePoint:
+ nanoseconds = float(node.get_clock().now().nanoseconds)
+ seconds = nanoseconds / 1e9
+ dt = datetime.datetime.fromtimestamp(seconds, timezone)
+ return ScheduleTimePoint(dt.hour, dt.minute)
+
+
+def main(argv=sys.argv):
+ rclpy.init(args=argv)
+ node = Node("rmf_charging_schedule")
+ use_sim_time = node.get_parameter('use_sim_time').value
+
+ args_without_ros = rclpy.utilities.remove_ros_args(argv)
+
+ parser = argparse.ArgumentParser(
+ prog='rmf_charging_schedule',
+ description='Manage a fixed 24-hour charger schedule rotation',
+ )
+ parser.add_argument(
+ 'schedule', type=str,
+ help=(
+ 'A .yaml file representing the schedule. See README for the '
+ 'expected format.'
+ )
+ )
+ parser.add_argument(
+ '-z', '--timezone', type=str, required=False,
+ help=(
+ 'Timezone that the 24-hour rotation will be based on. If not '
+ 'provided, the system\'s local timezone will be used.'
+ )
+ )
+ parser.add_argument(
+ '-t', '--test-time', action='store_true',
+ help=(
+ 'Use this option to test the real time calculation by printing the '
+ 'current HH:MM based on your settings. This may be used in '
+ 'conjunction with the --timezone option and sim time. The node '
+ 'will immediately quit after printing the time, so this will not '
+ 'publish any assignment messages.'
+ )
+ )
+
+ args = parser.parse_args(args_without_ros[1:])
+ schedule_file = args.schedule
+
+ if args.timezone is not None:
+ timezone = ZoneInfo(args.timezone)
+ else:
+ timezone = None
+
+ if use_sim_time:
+ get_time = lambda: simulation_time(node)
+ else:
+ get_time = lambda: real_time(node, timezone)
+
+ if args.test_time:
+ t = get_time()
+ print(f'{t.hour:02d}:{t.minute:02d}')
+ return
+
+ with open(schedule_file, 'r') as f:
+ schedule_yaml = yaml.safe_load(f)
+
+ unsorted_schedule = {}
+ for fleet, change in schedule_yaml.items():
+ for time_text, assignments in change.items():
+ time = ScheduleTimePoint.parse(time_text)
+ entry: list[Assignment] = unsorted_schedule.get(time, list())
+ for robot, charger in assignments.items():
+ entry.append(Assignment(fleet, robot, charger))
+ unsorted_schedule[time] = entry
+
+ schedule = {}
+ sorted_times = []
+ for time in sorted(unsorted_schedule.keys()):
+ sorted_times.append(time)
+ schedule[time] = unsorted_schedule[time]
+
+ num_fleets = len(schedule_yaml.keys())
+ transient_qos = QoSProfile(
+ history=QoSHistoryPolicy.KEEP_LAST,
+ depth=2*num_fleets,
+ reliability=QoSReliabilityPolicy.RELIABLE,
+ durability=QoSDurabilityPolicy.TRANSIENT_LOCAL,
+ )
+ publisher = node.create_publisher(
+ ChargingAssignments, 'charging_assignments', transient_qos
+ )
+
+ # fleet -> robot -> charger
+ assignments = {}
+ last_update_index = bisect.bisect_right(sorted_times, get_time())
+ update_assignments(
+ None, last_update_index,
+ sorted_times, schedule, assignments, publisher, node,
+ )
+
+ def update():
+ nonlocal last_update_index
+ nonlocal sorted_times
+ nonlocal schedule
+ nonlocal assignments
+ nonlocal publisher
+
+ next_update_index = bisect.bisect_right(sorted_times, get_time())
+ if last_update_index < next_update_index:
+ update_assignments(
+ last_update_index, next_update_index,
+ sorted_times, schedule, assignments, publisher, node,
+ )
+ last_update_index = next_update_index
+
+ elif next_update_index < last_update_index:
+ # The cycle must have restarted, e.g. passing midnight
+ update_assignments(
+ None, next_update_index,
+ sorted_times, schedule, assignments, publisher, node,
+ )
+ last_update_index = next_update_index
+
+ node.create_timer(10.0, update)
+
+ rclpy.spin(node)
+
+if __name__ == '__main__':
+ main(sys.argv)
diff --git a/rmf_charging_schedule/setup.cfg b/rmf_charging_schedule/setup.cfg
new file mode 100644
index 000000000..fb5d24c84
--- /dev/null
+++ b/rmf_charging_schedule/setup.cfg
@@ -0,0 +1,4 @@
+[develop]
+script_dir=$base/lib/rmf_charging_schedule
+[install]
+install_scripts=$base/lib/rmf_charging_schedule
diff --git a/rmf_charging_schedule/setup.py b/rmf_charging_schedule/setup.py
new file mode 100644
index 000000000..6ae87a05e
--- /dev/null
+++ b/rmf_charging_schedule/setup.py
@@ -0,0 +1,25 @@
+from setuptools import setup, find_packages
+
+package_name = 'rmf_charging_schedule'
+
+setup(
+ name=package_name,
+ version='2.3.2',
+ packages=find_packages(),
+ data_files=[
+ ('share/ament_index/resource_index/packages',
+ ['resource/' + package_name]),
+ ],
+ install_requires=['setuptools'],
+ zip_safe=True,
+ maintainer='Grey',
+ maintainer_email='mxgrey@intrinsic.ai',
+ description='A node that manages a fixed schedule for robot charger usage',
+ license='Apache License 2.0',
+ tests_require=['pytest'],
+ entry_points={
+ 'console_scripts': [
+ 'charging_schedule=rmf_charging_schedule.main:main',
+ ],
+ },
+)
diff --git a/rmf_fleet_adapter/CMakeLists.txt b/rmf_fleet_adapter/CMakeLists.txt
index c38326902..152250741 100644
--- a/rmf_fleet_adapter/CMakeLists.txt
+++ b/rmf_fleet_adapter/CMakeLists.txt
@@ -255,6 +255,25 @@ target_include_directories(lift_supervisor
# -----------------------------------------------------------------------------
+add_executable(mutex_group_supervisor
+ src/mutex_group_supervisor/main.cpp
+)
+
+target_link_libraries(mutex_group_supervisor
+ PRIVATE
+ rmf_fleet_adapter
+ ${rclcpp_LIBARRIES}
+ ${rmf_fleet_msgs_LIBRARIES}
+)
+
+target_include_directories(mutex_group_supervisor
+ PRIVATE
+ ${rclcpp_INCLUDE_DIRS}
+ ${rmf_fleet_msgs_INCLUDE_DIRS}
+)
+
+# -----------------------------------------------------------------------------
+
add_executable(experimental_lift_watchdog
src/experimental_lift_watchdog/main.cpp
)
@@ -502,6 +521,7 @@ install(
mock_traffic_light
full_control
lift_supervisor
+ mutex_group_supervisor
experimental_lift_watchdog
door_supervisor
robot_state_aggregator
diff --git a/rmf_fleet_adapter/include/rmf_fleet_adapter/StandardNames.hpp b/rmf_fleet_adapter/include/rmf_fleet_adapter/StandardNames.hpp
index 9d0ac0591..a736ce9e5 100644
--- a/rmf_fleet_adapter/include/rmf_fleet_adapter/StandardNames.hpp
+++ b/rmf_fleet_adapter/include/rmf_fleet_adapter/StandardNames.hpp
@@ -68,6 +68,13 @@ const std::string InterruptRequestTopicName = "robot_interrupt_request";
const std::string TaskApiRequests = "task_api_requests";
const std::string TaskApiResponses = "task_api_responses";
+const std::string ChargingAssignmentsTopicName = "charging_assignments";
+
+const std::string MutexGroupRequestTopicName = "mutex_group_request";
+const std::string MutexGroupStatesTopicName = "mutex_group_states";
+
+const uint64_t Unclaimed = (uint64_t)(-1);
+
} // namespace rmf_fleet_adapter
#endif // RMF_FLEET_ADAPTER__STANDARDNAMES_HPP
diff --git a/rmf_fleet_adapter/include/rmf_fleet_adapter/agv/EasyFullControl.hpp b/rmf_fleet_adapter/include/rmf_fleet_adapter/agv/EasyFullControl.hpp
index b5f9531c9..2be6c0698 100644
--- a/rmf_fleet_adapter/include/rmf_fleet_adapter/agv/EasyFullControl.hpp
+++ b/rmf_fleet_adapter/include/rmf_fleet_adapter/agv/EasyFullControl.hpp
@@ -74,7 +74,11 @@ class EasyFullControl : public std::enable_shared_from_this
class CommandExecution;
/// Signature for a function that handles navigation requests. The request
- /// will specify an (x, y) location and yaw on a map.
+ /// will specify a destination for the robot to go to.
+ ///
+ /// \param[in] destination
+ /// Where the robot should move to. Includings (x, y) coordinates, a target
+ /// yaw, a map name, and may include a graph index when one is available.
///
/// \param[in] execution
/// The command execution progress updater. Use this to keep the fleet
@@ -87,6 +91,19 @@ class EasyFullControl : public std::enable_shared_from_this
/// Signature for a function to handle stop requests.
using StopRequest = std::function;
+ /// Signature for a function that handles localization requests. The request
+ /// will specify an approximate location for the robot.
+ ///
+ /// \param[in] location_estimate
+ /// An estimate for where the robot is currently located.
+ ///
+ /// \param[in] execution
+ /// The command execution progress updater. Use this to keep the fleet
+ /// adapter updated on the progress of localizing.
+ using LocalizationRequest = std::function;
+
/// Add a robot to the fleet once it is available.
///
/// \param[in] name
@@ -333,6 +350,13 @@ class EasyFullControl::RobotCallbacks
/// Get the action executor.
ActionExecutor action_executor() const;
+ /// Give the robot a localization callback. Unlike the callbacks used by the
+ /// constructor, this callback is optional.
+ RobotCallbacks& with_localization(LocalizationRequest localization);
+
+ /// Get the callback for localizing if available.
+ LocalizationRequest localize() const;
+
class Implementation;
private:
rmf_utils::impl_ptr _pimpl;
@@ -414,6 +438,10 @@ class EasyFullControl::Destination
/// from this field.
std::optional graph_index() const;
+ /// The name of this destination, if it has one. Nameless destinations will
+ /// give an empty string.
+ std::string name() const;
+
/// If there is a speed limit that should be respected while approaching the
/// destination, this will indicate it.
std::optional speed_limit() const;
@@ -422,6 +450,10 @@ class EasyFullControl::Destination
/// will contain the name of the dock.
std::optional dock() const;
+ /// Get whether the destination is inside of a lift, and if so get the
+ /// properties of the lift.
+ rmf_traffic::agv::Graph::LiftPropertiesPtr inside_lift() const;
+
class Implementation;
private:
Destination();
@@ -743,6 +775,38 @@ class EasyFullControl::FleetConfiguration
/// Set the minimum lane length.
void set_default_min_lane_length(double distance);
+ /// During a fire emergency, real-life lifts might be required to move to a
+ /// specific level and refuse to stop or go to any other level. This function
+ /// lets you provide this information to the fleet adapter so that it can
+ /// produce reasonable emergency pullover plans for robots that happen to be
+ /// inside of a lift when the fire alarm goes off.
+ ///
+ /// Internally, this will close all lanes that go into the specified lift and
+ /// close all lanes exiting this lift (except on the designated level) when a
+ /// fire emergency begins. Lifts that were not specified in a call to this
+ /// function will not behave any differently during a fire emergency.
+ ///
+ /// \param[in] lift_name
+ /// The name of the lift whose behavior is being specified
+ ///
+ /// \param[in] emergency_level_name
+ /// The level that lift will go to when a fire emergency is happening
+ void set_lift_emergency_level(
+ std::string lift_name,
+ std::string emergency_level_name);
+
+ /// Get mutable access to the level that each specified lift will go to during
+ /// a fire emergency.
+ ///
+ /// \sa set_lift_emergency_level
+ std::unordered_map& change_lift_emergency_levels();
+
+ /// Get the level that each specified lift will go to during a fire emergency.
+ ///
+ /// \sa set_lift_emergency_level
+ const std::unordered_map&
+ lift_emergency_levels() const;
+
class Implementation;
private:
rmf_utils::impl_ptr _pimpl;
diff --git a/rmf_fleet_adapter/include/rmf_fleet_adapter/agv/FleetUpdateHandle.hpp b/rmf_fleet_adapter/include/rmf_fleet_adapter/agv/FleetUpdateHandle.hpp
index 4bd5ad23d..290f5bd35 100644
--- a/rmf_fleet_adapter/include/rmf_fleet_adapter/agv/FleetUpdateHandle.hpp
+++ b/rmf_fleet_adapter/include/rmf_fleet_adapter/agv/FleetUpdateHandle.hpp
@@ -210,6 +210,26 @@ class FleetUpdateHandle : public std::enable_shared_from_this
/// Specify a set of lanes that should be open.
void open_lanes(std::vector lane_indices);
+ /// During a fire emergency, real-life lifts might be required to move to a
+ /// specific level and refuse to stop or go to any other level. This function
+ /// lets you provide this information to the fleet adapter so that it can
+ /// produce reasonable emergency pullover plans for robots that happen to be
+ /// inside of a lift when the fire alarm goes off.
+ ///
+ /// Internally, this will close all lanes that go into the specified lift and
+ /// close all lanes exiting this lift (except on the designated level) when a
+ /// fire emergency begins. Lifts that were not specified in a call to this
+ /// function will not behave any differently during a fire emergency.
+ ///
+ /// \param[in] lift_name
+ /// The name of the lift whose behavior is being specified
+ ///
+ /// \param[in] emergency_level_name
+ /// The level that lift will go to when a fire emergency is happening
+ void set_lift_emergency_level(
+ std::string lift_name,
+ std::string emergency_level_name);
+
/// A class used to describe speed limit imposed on lanes.
class SpeedLimitRequest
{
diff --git a/rmf_fleet_adapter/include/rmf_fleet_adapter/agv/RobotUpdateHandle.hpp b/rmf_fleet_adapter/include/rmf_fleet_adapter/agv/RobotUpdateHandle.hpp
index d0aa2c22e..22da7b444 100644
--- a/rmf_fleet_adapter/include/rmf_fleet_adapter/agv/RobotUpdateHandle.hpp
+++ b/rmf_fleet_adapter/include/rmf_fleet_adapter/agv/RobotUpdateHandle.hpp
@@ -126,6 +126,10 @@ class RobotUpdateHandle
/// value that was given to the setter.
rmf_utils::optional maximum_delay() const;
+ /// Get the current task ID of the robot, or an empty string if the robot
+ /// is not performing any task.
+ const std::string current_task_id() const;
+
/// Unique identifier for an activity that the robot is performing. Used by
/// the EasyFullControl API.
class ActivityIdentifier
@@ -404,6 +408,9 @@ class RobotUpdateHandle
/// By default this behavior is enabled.
void enable_responsive_wait(bool value);
+ /// If the robot is holding onto a session with a lift, release that session.
+ void release_lift();
+
class Implementation;
/// This API is experimental and will not be supported in the future. Users
@@ -471,6 +478,9 @@ class RobotUpdateHandle
Watchdog watchdog,
rmf_traffic::Duration wait_duration = std::chrono::seconds(10));
+ /// Turn on/off a debug dump of how position updates are being processed
+ void debug_positions(bool on);
+
private:
friend Implementation;
Implementation* _pimpl;
diff --git a/rmf_fleet_adapter/include/rmf_fleet_adapter/tasks/ParkRobotIndefinitely.hpp b/rmf_fleet_adapter/include/rmf_fleet_adapter/tasks/ParkRobotIndefinitely.hpp
new file mode 100644
index 000000000..01a2d315b
--- /dev/null
+++ b/rmf_fleet_adapter/include/rmf_fleet_adapter/tasks/ParkRobotIndefinitely.hpp
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2023 Open Source Robotics Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+*/
+
+#ifndef RMF_FLEET_ADAPTER__TASKS__PARKROBOT_HPP
+#define RMF_FLEET_ADAPTER__TASKS__PARKROBOT_HPP
+
+#include
+
+namespace rmf_fleet_adapter {
+namespace tasks {
+
+//==============================================================================
+/// Use this task factory to make finisher tasks (idle tasks) that will move the
+/// robot to a parking spot.
+class ParkRobotIndefinitely : public rmf_task::RequestFactory
+{
+public:
+ /// Constructor
+ ///
+ /// \param[in] requester
+ /// The identifier of the entity that owns this RequestFactory, that will be
+ /// the designated requester of each new request.
+ ///
+ /// \param[in] time_now_cb
+ /// Callback function that returns the current time.
+ ///
+ /// \param[in] parking_waypoint
+ /// The graph index of the waypoint assigned to this AGV for parking.
+ /// If nullopt, the AGV will return to its charging_waypoint and remain idle
+ /// there. It will not wait for its battery to charge up before undertaking
+ /// new tasks.
+ ParkRobotIndefinitely(
+ const std::string& requester,
+ std::function time_now_cb,
+ std::optional parking_waypoint = std::nullopt);
+
+ rmf_task::ConstRequestPtr make_request(
+ const rmf_task::State& state) const final;
+
+ class Implementation;
+private:
+ rmf_utils::impl_ptr _pimpl;
+};
+
+} // namespace tasks
+} // namespace rmf_fleet_adapter
+
+#endif // RMF_FLEET_ADAPTER__AGV__PARKROBOT_HPP
diff --git a/rmf_fleet_adapter/src/full_control/main.cpp b/rmf_fleet_adapter/src/full_control/main.cpp
index e979212fd..2888482da 100644
--- a/rmf_fleet_adapter/src/full_control/main.cpp
+++ b/rmf_fleet_adapter/src/full_control/main.cpp
@@ -1249,10 +1249,12 @@ std::shared_ptr make_fleet(
if (finishing_request_string == "charge")
{
- finishing_request =
+ auto charge_factory =
std::make_shared(
std::string(node->get_name()),
std::move(get_time));
+ charge_factory->set_indefinite(true);
+ finishing_request = charge_factory;
RCLCPP_INFO(
node->get_logger(),
"Fleet is configured to perform ChargeBattery as finishing request");
diff --git a/rmf_fleet_adapter/src/lift_supervisor/Node.cpp b/rmf_fleet_adapter/src/lift_supervisor/Node.cpp
index fc096dcd0..77c05fdcb 100644
--- a/rmf_fleet_adapter/src/lift_supervisor/Node.cpp
+++ b/rmf_fleet_adapter/src/lift_supervisor/Node.cpp
@@ -28,12 +28,14 @@ Node::Node()
: rclcpp::Node("rmf_lift_supervisor")
{
const auto default_qos = rclcpp::SystemDefaultsQoS();
+ const auto transient_qos = rclcpp::SystemDefaultsQoS()
+ .reliable().keep_last(100).transient_local();
_lift_request_pub = create_publisher(
- FinalLiftRequestTopicName, default_qos);
+ FinalLiftRequestTopicName, transient_qos);
_adapter_lift_request_sub = create_subscription(
- AdapterLiftRequestTopicName, default_qos,
+ AdapterLiftRequestTopicName, transient_qos,
[&](LiftRequest::UniquePtr msg)
{
_adapter_lift_request_update(std::move(msg));
@@ -53,6 +55,12 @@ Node::Node()
//==============================================================================
void Node::_adapter_lift_request_update(LiftRequest::UniquePtr msg)
{
+
+ RCLCPP_INFO(
+ this->get_logger(),
+ "[%s] Received adapter lift request to [%s] with request type [%d]",
+ msg->session_id.c_str(), msg->destination_floor.c_str(), msg->request_type
+ );
auto& curr_request = _active_sessions.insert(
std::make_pair(msg->lift_name, nullptr)).first->second;
@@ -64,7 +72,13 @@ void Node::_adapter_lift_request_update(LiftRequest::UniquePtr msg)
curr_request = std::move(msg);
else
{
+ msg->request_time = this->now();
_lift_request_pub->publish(*msg);
+ RCLCPP_INFO(
+ this->get_logger(),
+ "[%s] Published end lift session from lift supervisor",
+ msg->session_id.c_str()
+ );
curr_request = nullptr;
}
}
@@ -90,7 +104,25 @@ void Node::_lift_state_update(LiftState::UniquePtr msg)
{
if ((lift_request->destination_floor != msg->current_floor) ||
(lift_request->door_state != msg->door_state))
- _lift_request_pub->publish(*lift_request);
+ lift_request->request_time = this->now();
+ _lift_request_pub->publish(*lift_request);
+ RCLCPP_INFO(
+ this->get_logger(),
+ "[%s] Published lift request to [%s] from lift supervisor",
+ msg->session_id.c_str(), lift_request->destination_floor.c_str()
+ );
+ }
+ else
+ {
+ // If there are no active sessions going on, we keep publishing session
+ // end requests to ensure that the lift is released
+ LiftRequest request;
+ request.lift_name = msg->lift_name;
+ request.destination_floor = msg->current_floor;
+ request.session_id = msg->session_id;
+ request.request_time = this->now();
+ request.request_type = LiftRequest::REQUEST_END_SESSION;
+ _lift_request_pub->publish(request);
}
// For now, we do not need to publish this.
diff --git a/rmf_fleet_adapter/src/mutex_group_supervisor/main.cpp b/rmf_fleet_adapter/src/mutex_group_supervisor/main.cpp
new file mode 100644
index 000000000..4f07df44d
--- /dev/null
+++ b/rmf_fleet_adapter/src/mutex_group_supervisor/main.cpp
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2023 Open Source Robotics Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+*/
+
+#include
+
+#include
+#include
+#include
+
+#include
+#include
+
+#include
+#include
+#include
+
+struct TimeStamps
+{
+ rclcpp::Time claim_time;
+ std::chrono::steady_clock::time_point heartbeat_time;
+};
+
+using ClaimMap = std::unordered_map;
+using MutexGroupRequest = rmf_fleet_msgs::msg::MutexGroupRequest;
+using MutexGroupStates = rmf_fleet_msgs::msg::MutexGroupStates;
+using MutextGroupAssignment = rmf_fleet_msgs::msg::MutexGroupAssignment;
+
+const uint64_t Unclaimed = rmf_fleet_adapter::Unclaimed;
+
+class Node : public rclcpp::Node
+{
+public:
+ Node()
+ : rclcpp::Node("mutex_group_supervisor")
+ {
+ const auto qos = rclcpp::SystemDefaultsQoS()
+ .reliable()
+ .transient_local()
+ .keep_last(100);
+
+ latest_states = rmf_fleet_msgs::build()
+ .assignments({});
+
+ request_sub = create_subscription(
+ rmf_fleet_adapter::MutexGroupRequestTopicName,
+ qos,
+ [&](const MutexGroupRequest& request)
+ {
+ handle_request(request);
+ });
+
+ state_pub = create_publisher(
+ rmf_fleet_adapter::MutexGroupStatesTopicName,
+ qos);
+
+ heartbeat_timer = create_wall_timer(
+ std::chrono::seconds(2),
+ [&]() { do_heartbeat(); });
+ }
+
+ void handle_request(const MutexGroupRequest& request)
+ {
+ auto& claims = mutex_groups[request.group];
+ if (request.mode == request.MODE_RELEASE)
+ {
+ const auto g_it = mutex_groups.find(request.group);
+ if (g_it != mutex_groups.end())
+ {
+ const auto c_it = g_it->second.find(request.claimant);
+ if (c_it != g_it->second.end())
+ {
+ if (c_it->second.claim_time <= request.claim_time)
+ {
+ g_it->second.erase(c_it);
+ pick_next(request.group);
+ state_pub->publish(latest_states);
+ }
+ }
+ }
+ return;
+ }
+
+ auto now = std::chrono::steady_clock::now();
+ auto claim_time = rclcpp::Time(request.claim_time);
+ auto timestamps = TimeStamps { request.claim_time, now };
+ claims.insert_or_assign(request.claimant, timestamps);
+ for (const auto& s : latest_states.assignments)
+ {
+ if (s.group == request.group && s.claimant != Unclaimed)
+ {
+ check_for_conflicts();
+ return;
+ }
+ }
+ pick_next(request.group);
+ }
+
+ void do_heartbeat()
+ {
+ const auto now = std::chrono::steady_clock::now();
+ // TODO(MXG): Make this timeout configurable
+ const auto timeout = std::chrono::seconds(10);
+ for (auto& [group, claims] : mutex_groups)
+ {
+ std::vector remove_claims;
+ for (const auto& [claimant, timestamp] : claims)
+ {
+ if (timestamp.heartbeat_time + timeout < now)
+ {
+ remove_claims.push_back(claimant);
+ }
+ }
+
+ uint64_t current_claimer = Unclaimed;
+ for (const auto& assignment : latest_states.assignments)
+ {
+ if (assignment.group == group)
+ {
+ current_claimer = assignment.claimant;
+ break;
+ }
+ }
+
+ bool need_next_pick = false;
+ for (const auto& remove_claim : remove_claims)
+ {
+ if (current_claimer == remove_claim)
+ {
+ need_next_pick = true;
+ }
+
+ claims.erase(remove_claim);
+ }
+
+ if (need_next_pick)
+ {
+ pick_next(group);
+ }
+ }
+
+ state_pub->publish(latest_states);
+ }
+
+ struct ClaimList
+ {
+ std::unordered_set groups;
+ std::optional earliest_time;
+
+ void insert(std::string group, rclcpp::Time time)
+ {
+ groups.insert(std::move(group));
+ if (!earliest_time.has_value() || time < *earliest_time)
+ {
+ earliest_time = time;
+ }
+ }
+
+ void normalize(
+ uint64_t claimant,
+ std::unordered_map& mutex_groups) const
+ {
+ if (!earliest_time.has_value())
+ return;
+
+ for (const auto& group : groups)
+ {
+ mutex_groups[group][claimant].claim_time = *earliest_time;
+ }
+ }
+ };
+
+ void check_for_conflicts()
+ {
+ std::unordered_map claims;
+ for (const auto& [group, group_claims] : mutex_groups)
+ {
+ for (const auto& [claimant, timestamps] : group_claims)
+ {
+ claims[claimant].insert(group, timestamps.claim_time);
+ }
+ }
+
+ std::unordered_set normalized_groups;
+ for (auto i = claims.begin(); i != claims.end(); ++i)
+ {
+ auto j = i;
+ ++j;
+ for (; j != claims.end(); ++j)
+ {
+ const auto& claim_i = i->second;
+ const auto& claim_j = j->second;
+ if (claim_i.groups == claim_j.groups && claim_i.groups.size() > 1)
+ {
+ claim_i.normalize(i->first, mutex_groups);
+ claim_j.normalize(j->first, mutex_groups);
+
+ std::stringstream ss;
+ for (const auto& group : claim_i.groups)
+ {
+ normalized_groups.insert(group);
+ ss << "[" << group << "]";
+ }
+
+ RCLCPP_INFO(
+ get_logger(),
+ "Resolving mutex conflict between claimants [%lu] and [%lu] which both "
+ "want the mutex combination %s",
+ i->first,
+ j->first,
+ ss.str().c_str());
+ }
+ }
+ }
+
+ for (const auto& group : normalized_groups)
+ pick_next(group);
+ }
+
+ void pick_next(const std::string& group)
+ {
+ const auto& claimants = mutex_groups[group];
+ std::optional> earliest;
+ for (const auto& [claimant, timestamp] : claimants)
+ {
+ const auto& t = timestamp.claim_time;
+ if (!earliest.has_value() || t < earliest->first)
+ {
+ earliest = std::make_pair(t, claimant);
+ }
+ }
+
+ uint64_t claimant = Unclaimed;
+ builtin_interfaces::msg::Time claim_time;
+ if (earliest.has_value())
+ {
+ claim_time = earliest->first;
+ claimant = earliest->second;
+ }
+ bool group_found = false;
+ for (auto& a : latest_states.assignments)
+ {
+ if (a.group == group)
+ {
+ a.claimant = claimant;
+ a.claim_time = claim_time;
+ group_found = true;
+ break;
+ }
+ }
+ if (!group_found)
+ {
+ latest_states.assignments.push_back(
+ rmf_fleet_msgs::build()
+ .group(group)
+ .claimant(claimant)
+ .claim_time(claim_time));
+ }
+ }
+
+ std::unordered_map mutex_groups;
+ rclcpp::Subscription::SharedPtr request_sub;
+ rclcpp::Publisher::SharedPtr state_pub;
+ rclcpp::TimerBase::SharedPtr heartbeat_timer;
+ MutexGroupStates latest_states;
+};
+
+int main(int argc, char* argv[])
+{
+ rclcpp::init(argc, argv);
+ rclcpp::spin(std::make_shared());
+ rclcpp::shutdown();
+}
diff --git a/rmf_fleet_adapter/src/rmf_fleet_adapter/LegacyTask.hpp b/rmf_fleet_adapter/src/rmf_fleet_adapter/LegacyTask.hpp
index c580924da..9c205e34c 100644
--- a/rmf_fleet_adapter/src/rmf_fleet_adapter/LegacyTask.hpp
+++ b/rmf_fleet_adapter/src/rmf_fleet_adapter/LegacyTask.hpp
@@ -34,6 +34,8 @@
namespace rmf_fleet_adapter {
+using PlanIdPtr = std::shared_ptr;
+
//==============================================================================
class LegacyTask : public std::enable_shared_from_this
{
diff --git a/rmf_fleet_adapter/src/rmf_fleet_adapter/TaskManager.cpp b/rmf_fleet_adapter/src/rmf_fleet_adapter/TaskManager.cpp
index cbda9b141..67d9cc15e 100644
--- a/rmf_fleet_adapter/src/rmf_fleet_adapter/TaskManager.cpp
+++ b/rmf_fleet_adapter/src/rmf_fleet_adapter/TaskManager.cpp
@@ -73,6 +73,10 @@ TaskManagerPtr TaskManager::make(
std::optional> broadcast_client,
std::weak_ptr fleet_handle)
{
+ std::shared_ptr fleet = fleet_handle.lock();
+ if (!fleet)
+ return nullptr;
+
auto mgr = TaskManagerPtr(
new TaskManager(
std::move(context),
@@ -96,6 +100,7 @@ TaskManagerPtr TaskManager::make(
auto task_id = "emergency_pullover." + self->_context->name() + "."
+ self->_context->group() + "-"
+ std::to_string(self->_count_emergency_pullover++);
+ self->_context->current_task_id(task_id);
// TODO(MXG): Consider subscribing to the emergency pullover update
self->_emergency_pullover = ActiveTask::start(
@@ -115,18 +120,19 @@ TaskManagerPtr TaskManager::make(
});
};
- mgr->_emergency_sub = mgr->_context->node()->emergency_notice()
+ mgr->_emergency_sub = agv::FleetUpdateHandle::Implementation::get(*fleet)
+ .emergency_obs
.observe_on(rxcpp::identity_same_worker(mgr->_context->worker()))
.subscribe(
- [w = mgr->weak_from_this(), begin_pullover](const auto& msg)
+ [w = mgr->weak_from_this(), begin_pullover](const bool is_emergency)
{
if (auto mgr = w.lock())
{
- if (mgr->_emergency_active == msg->data)
+ if (mgr->_emergency_active == is_emergency)
return;
- mgr->_emergency_active = msg->data;
- if (msg->data)
+ mgr->_emergency_active = is_emergency;
+ if (is_emergency)
{
if (mgr->_waiting)
{
@@ -905,6 +911,20 @@ void TaskManager::enable_responsive_wait(bool value)
}
}
+//==============================================================================
+void TaskManager::set_idle_task(rmf_task::ConstRequestFactoryPtr task)
+{
+ if (_idle_task == task)
+ return;
+
+ _idle_task = std::move(task);
+ std::lock_guard guard(_mutex);
+ if (!_active_task && _queue.empty() && _direct_queue.empty())
+ {
+ _begin_waiting();
+ }
+}
+
//==============================================================================
void TaskManager::set_queue(
const std::vector& assignments)
@@ -1268,7 +1288,7 @@ void TaskManager::_begin_next_task()
if (_queue.empty() && _direct_queue.empty())
{
- if (!_waiting)
+ if (!_waiting && !_finished_waiting)
_begin_waiting();
return;
@@ -1309,6 +1329,7 @@ void TaskManager::_begin_next_task()
id.c_str(),
_context->requester_id().c_str());
+ _finished_waiting = false;
_context->current_task_end_state(assignment.finish_state());
_context->current_task_id(id);
_active_task = ActiveTask::start(
@@ -1360,7 +1381,7 @@ void TaskManager::_begin_next_task()
}
else
{
- if (!_waiting)
+ if (!_waiting && !_finished_waiting)
_begin_waiting();
}
@@ -1457,6 +1478,23 @@ std::function TaskManager::_robot_interruption_callback()
//==============================================================================
void TaskManager::_begin_waiting()
{
+ if (_idle_task)
+ {
+ const auto request = _idle_task->make_request(_context->make_get_state()());
+ _waiting = ActiveTask::start(
+ _context->task_activator()->activate(
+ _context->make_get_state(),
+ _context->task_parameters(),
+ *request,
+ _update_cb(),
+ _checkpoint_cb(),
+ _phase_finished_cb(),
+ _make_resume_from_waiting()),
+ _context->now());
+ _context->current_task_id(request->booking()->id());
+ return;
+ }
+
if (!_responsive_wait_enabled)
return;
@@ -1500,6 +1538,7 @@ void TaskManager::_begin_waiting()
_update_cb(),
_make_resume_from_waiting()),
_context->now());
+ _context->current_task_id(task_id);
}
//==============================================================================
@@ -1564,6 +1603,7 @@ std::function TaskManager::_make_resume_from_waiting()
if (!self)
return;
+ self->_finished_waiting = true;
self->_waiting = ActiveTask();
self->_begin_next_task();
});
@@ -2151,6 +2191,7 @@ std::function TaskManager::_task_finished(std::string id)
// Publish the final state of the task before destructing it
self->_publish_task_state();
self->_active_task = ActiveTask();
+ self->_context->current_task_id(std::nullopt);
self->_context->worker().schedule(
[w = self->weak_from_this()](const auto&)
diff --git a/rmf_fleet_adapter/src/rmf_fleet_adapter/TaskManager.hpp b/rmf_fleet_adapter/src/rmf_fleet_adapter/TaskManager.hpp
index 1d8759bcc..52de08f81 100644
--- a/rmf_fleet_adapter/src/rmf_fleet_adapter/TaskManager.hpp
+++ b/rmf_fleet_adapter/src/rmf_fleet_adapter/TaskManager.hpp
@@ -111,6 +111,8 @@ class TaskManager : public std::enable_shared_from_this
void enable_responsive_wait(bool value);
+ void set_idle_task(rmf_task::ConstRequestFactoryPtr task);
+
/// Set the queue for this task manager with assignments generated from the
/// task planner
void set_queue(const std::vector& assignments);
@@ -308,6 +310,9 @@ class TaskManager : public std::enable_shared_from_this
std::weak_ptr _fleet_handle;
rmf_task::ConstActivatorPtr _task_activator;
ActiveTask _active_task;
+ /// The task that should be performed when the robot is idle. This takes
+ /// precedent over responsive waiting.
+ rmf_task::ConstRequestFactoryPtr _idle_task;
bool _responsive_wait_enabled = false;
bool _emergency_active = false;
std::optional _emergency_pullover_interrupt_token;
@@ -327,6 +332,7 @@ class TaskManager : public std::enable_shared_from_this
/// will ensure that the agent continues to respond to traffic negotiations so
/// it does not become a blocker for other traffic participants.
ActiveTask _waiting;
+ bool _finished_waiting = false;
uint16_t _count_waiting = 0;
// TODO: Eliminate the need for a mutex by redesigning the use of the task
diff --git a/rmf_fleet_adapter/src/rmf_fleet_adapter/agv/Adapter.cpp b/rmf_fleet_adapter/src/rmf_fleet_adapter/agv/Adapter.cpp
index de6eb68e1..2bb577397 100644
--- a/rmf_fleet_adapter/src/rmf_fleet_adapter/agv/Adapter.cpp
+++ b/rmf_fleet_adapter/src/rmf_fleet_adapter/agv/Adapter.cpp
@@ -264,7 +264,7 @@ std::shared_ptr Adapter::add_easy_fleet(
if (entry)
entry->execute(finder);
- const auto* exit = config.graph()->get_lane(i).entry().event();
+ const auto* exit = config.graph()->get_lane(i).exit().event();
if (exit)
exit->execute(finder);
}
@@ -367,6 +367,11 @@ std::shared_ptr Adapter::add_easy_fleet(
*config.transformations_to_robot_coordinates());
}
+ for (const auto& [lift, level] : config.lift_emergency_levels())
+ {
+ fleet_handle->set_lift_emergency_level(lift, level);
+ }
+
return EasyFullControl::Implementation::make(
fleet_handle,
config.skip_rotation_commands(),
diff --git a/rmf_fleet_adapter/src/rmf_fleet_adapter/agv/EasyFullControl.cpp b/rmf_fleet_adapter/src/rmf_fleet_adapter/agv/EasyFullControl.cpp
index a60f57043..bc9db015c 100644
--- a/rmf_fleet_adapter/src/rmf_fleet_adapter/agv/EasyFullControl.cpp
+++ b/rmf_fleet_adapter/src/rmf_fleet_adapter/agv/EasyFullControl.cpp
@@ -42,6 +42,7 @@
#include
#include
#include
+#include
#include
#include
@@ -217,6 +218,7 @@ class EasyFullControl::RobotCallbacks::Implementation
NavigationRequest navigate;
StopRequest stop;
ActionExecutor action_executor;
+ LocalizationRequest localize;
};
//==============================================================================
@@ -227,7 +229,8 @@ EasyFullControl::RobotCallbacks::RobotCallbacks(
: _pimpl(rmf_utils::make_impl(Implementation{
std::move(navigate),
std::move(stop),
- std::move(action_executor)
+ std::move(action_executor),
+ nullptr
}))
{
// Do nothing
@@ -252,334 +255,409 @@ auto EasyFullControl::RobotCallbacks::action_executor() const -> ActionExecutor
}
//==============================================================================
-class EasyFullControl::CommandExecution::Implementation
+auto EasyFullControl::RobotCallbacks::with_localization(
+ LocalizationRequest localization) -> RobotCallbacks&
+{
+ _pimpl->localize = std::move(localization);
+ return *this;
+}
+
+//==============================================================================
+auto EasyFullControl::RobotCallbacks::localize() const -> LocalizationRequest
+{
+ return _pimpl->localize;
+}
+
+//==============================================================================
+class EasyFullControl::CommandExecution::Implementation::Data
{
public:
- struct Data
+ std::vector waypoints;
+ std::vector lanes;
+ std::optional target_location;
+ std::optional final_orientation;
+ rmf_traffic::Duration planned_wait_time;
+ std::optional schedule_override;
+ std::shared_ptr nav_params;
+ std::function arrival_estimator;
+
+ void release_stubbornness()
{
- std::vector waypoints;
- std::vector lanes;
- std::optional final_orientation;
- rmf_traffic::Duration planned_wait_time;
- std::optional schedule_override;
- std::shared_ptr nav_params;
- std::function arrival_estimator;
+ if (schedule_override.has_value())
+ {
+ schedule_override->release_stubbornness();
+ }
+ }
- void release_stubbornness()
+ void update_location(
+ const std::shared_ptr& context,
+ const std::string& map,
+ Eigen::Vector3d location)
+ {
+ if (schedule_override.has_value())
{
- if (schedule_override.has_value())
- {
- schedule_override->release_stubbornness();
- }
+ return schedule_override->overridden_update(
+ context,
+ map,
+ location);
}
- void update_location(
- const std::shared_ptr& context,
- const std::string& map,
- Eigen::Vector3d location)
+ auto planner = context->planner();
+ if (!planner)
{
- if (schedule_override.has_value())
- {
- return schedule_override->overridden_update(
- context,
- map,
- location);
- }
+ RCLCPP_ERROR(
+ context->node()->get_logger(),
+ "Planner unavailable for robot [%s], cannot update its location",
+ context->requester_id().c_str());
+ return;
+ }
- auto planner = context->planner();
- if (!planner)
+ const auto& graph = planner->get_configuration().graph();
+ const rmf_traffic::agv::LaneClosure* closures =
+ context->get_lane_closures();
+ std::optional> on_waypoint;
+ auto p = Eigen::Vector2d(location[0], location[1]);
+ const double yaw = location[2];
+ for (std::size_t wp : waypoints)
+ {
+ if (wp >= graph.num_waypoints())
{
RCLCPP_ERROR(
context->node()->get_logger(),
- "Planner unavailable for robot [%s], cannot update its location",
- context->requester_id().c_str());
+ "Robot [%s] has a command with a waypoint [%lu] that is outside "
+ "the range of the graph [%lu]. We will not do a location update.",
+ context->requester_id().c_str(),
+ wp,
+ graph.num_waypoints());
+ // Should we also issue a replan command?
return;
}
- const auto& graph = planner->get_configuration().graph();
- const auto& closed_lanes = planner->get_configuration().lane_closures();
- std::optional> on_waypoint;
- auto p = Eigen::Vector2d(location[0], location[1]);
- const double yaw = location[2];
- for (std::size_t wp : waypoints)
+ const auto p_wp = graph.get_waypoint(wp).get_location();
+ auto dist = (p - p_wp).norm();
+ if (dist <= nav_params->max_merge_waypoint_distance)
+ {
+ if (!on_waypoint.has_value() || dist < on_waypoint->second)
+ {
+ on_waypoint = std::make_pair(wp, dist);
+ }
+ }
+ }
+
+ rmf_traffic::agv::Plan::StartSet starts;
+ const auto now = rmf_traffic_ros2::convert(context->node()->now());
+ if (on_waypoint.has_value())
+ {
+ const auto wp = on_waypoint->first;
+ starts.push_back(rmf_traffic::agv::Plan::Start(now, wp, yaw, p));
+ for (std::size_t lane_id : graph.lanes_from(wp))
{
- if (wp >= graph.num_waypoints())
+ if (lane_id >= graph.num_lanes())
{
RCLCPP_ERROR(
context->node()->get_logger(),
- "Robot [%s] has a command with a waypoint [%lu] that is outside "
- "the range of the graph [%lu]. We will not do a location update.",
+ "Nav graph for robot [%s] has an invalid lane ID [%lu] leaving "
+ "vertex [%lu], lane ID range is [%lu]. We will not do a location "
+ "update.",
context->requester_id().c_str(),
+ lane_id,
wp,
- graph.num_waypoints());
+ graph.num_lanes());
// Should we also issue a replan command?
return;
}
- const auto p_wp = graph.get_waypoint(wp).get_location();
- auto dist = (p - p_wp).norm();
- if (dist <= nav_params->max_merge_waypoint_distance)
+ if (closures && closures->is_closed(lane_id))
{
- if (!on_waypoint.has_value() || dist < on_waypoint->second)
- {
- on_waypoint = std::make_pair(wp, dist);
- }
+ // Don't use a lane that's closed
+ continue;
}
- }
- rmf_traffic::agv::Plan::StartSet starts;
- const auto now = rmf_traffic_ros2::convert(context->node()->now());
- if (on_waypoint.has_value())
+ auto wp_exit = graph.get_lane(lane_id).exit().waypoint_index();
+ starts.push_back(
+ rmf_traffic::agv::Plan::Start(now, wp_exit, yaw, p, lane_id));
+ }
+ }
+ else
+ {
+ std::optional> on_lane;
+ for (auto lane_id : lanes)
{
- const auto wp = on_waypoint->first;
- starts.push_back(rmf_traffic::agv::Plan::Start(now, wp, yaw, p));
- for (std::size_t lane_id : graph.lanes_from(wp))
+ if (lane_id >= graph.num_lanes())
{
- if (lane_id >= graph.num_lanes())
- {
- RCLCPP_ERROR(
- context->node()->get_logger(),
- "Nav graph for robot [%s] has an invalid lane ID [%lu] leaving "
- "vertex [%lu], lane ID range is [%lu]. We will not do a location "
- "update.",
- context->requester_id().c_str(),
- lane_id,
- wp,
- graph.num_lanes());
- // Should we also issue a replan command?
- return;
- }
+ RCLCPP_ERROR(
+ context->node()->get_logger(),
+ "Robot [%s] has a command with a lane [%lu] that is outside the "
+ "range of the graph [%lu]. We will not do a location update.",
+ context->requester_id().c_str(),
+ lane_id,
+ graph.num_lanes());
+ // Should we also issue a replan command?
+ return;
+ }
- if (closed_lanes.is_closed(lane_id))
- {
- // Don't use a lane that's closed
- continue;
- }
+ if (closures && closures->is_closed(lane_id))
+ {
+ continue;
+ }
- auto wp_exit = graph.get_lane(lane_id).exit().waypoint_index();
- starts.push_back(
- rmf_traffic::agv::Plan::Start(now, wp_exit, yaw, p, lane_id));
+ const auto& lane = graph.get_lane(lane_id);
+ const auto p0 =
+ graph.get_waypoint(lane.entry().waypoint_index()).get_location();
+ const auto p1 =
+ graph.get_waypoint(lane.exit().waypoint_index()).get_location();
+ const auto lane_length = (p1 - p0).norm();
+ double dist_to_lane = 0.0;
+ if (lane_length < nav_params->min_lane_length)
+ {
+ dist_to_lane = std::min(
+ (p - p0).norm(),
+ (p - p1).norm());
}
- }
- else
- {
- std::optional> on_lane;
- for (auto lane_id : lanes)
+ else
{
- if (lane_id >= graph.num_lanes())
- {
- RCLCPP_ERROR(
- context->node()->get_logger(),
- "Robot [%s] has a command with a lane [%lu] that is outside the "
- "range of the graph [%lu]. We will not do a location update.",
- context->requester_id().c_str(),
- lane_id,
- graph.num_lanes());
- // Should we also issue a replan command?
- return;
- }
-
- if (closed_lanes.is_closed(lane_id))
- {
- continue;
- }
-
- const auto& lane = graph.get_lane(lane_id);
- const auto p0 =
- graph.get_waypoint(lane.entry().waypoint_index()).get_location();
- const auto p1 =
- graph.get_waypoint(lane.exit().waypoint_index()).get_location();
- const auto lane_length = (p1 - p0).norm();
const auto lane_u = (p1 - p0)/lane_length;
const auto proj = (p - p0).dot(lane_u);
if (proj < 0.0 || lane_length < proj)
{
continue;
}
-
- const auto dist_to_lane = (p - p0 - proj * lane_u).norm();
- if (dist_to_lane <= nav_params->max_merge_lane_distance)
- {
- if (!on_lane.has_value() || dist_to_lane < on_lane->second)
- {
- on_lane = std::make_pair(lane_id, dist_to_lane);
- }
- }
+ dist_to_lane = (p - p0 - proj * lane_u).norm();
}
- if (on_lane.has_value())
+ if (dist_to_lane <= nav_params->max_merge_lane_distance)
{
- const auto lane_id = on_lane->first;
- const auto& lane = graph.get_lane(lane_id);
- const auto wp0 = lane.entry().waypoint_index();
- const auto wp1 = lane.exit().waypoint_index();
- starts.push_back(
- rmf_traffic::agv::Plan::Start(now, wp1, yaw, p, lane_id));
-
- if (const auto* reverse_lane = graph.lane_from(wp1, wp0))
+ if (!on_lane.has_value() || dist_to_lane < on_lane->second)
{
- starts.push_back(rmf_traffic::agv::Plan::Start(
- now, wp0, yaw, p, reverse_lane->index()));
+ on_lane = std::make_pair(lane_id, dist_to_lane);
}
}
- else
- {
- starts = nav_params->compute_plan_starts(graph, map, location, now);
- }
}
- if (!starts.empty())
+ if (on_lane.has_value())
{
- context->set_location(starts);
+ const auto lane_id = on_lane->first;
+ const auto& lane = graph.get_lane(lane_id);
+ const auto wp0 = lane.entry().waypoint_index();
+ const auto wp1 = lane.exit().waypoint_index();
+ starts.push_back(
+ rmf_traffic::agv::Plan::Start(now, wp1, yaw, p, lane_id));
+
+ if (const auto* reverse_lane = graph.lane_from(wp1, wp0))
+ {
+ starts.push_back(rmf_traffic::agv::Plan::Start(
+ now, wp0, yaw, p, reverse_lane->index()));
+ }
}
- else
+ }
+
+ if (starts.empty())
+ {
+ starts = nav_params->compute_plan_starts(graph, map, location, now);
+ }
+
+ if (!starts.empty())
+ {
+ if (context->debug_positions)
{
- context->set_lost(Location { now, map, location });
+ std::stringstream ss;
+ ss << __FILE__ << "|" << __LINE__ << ": " << starts.size()
+ << " starts:" << print_starts(starts, graph);
+ std::cout << ss.str() << std::endl;
}
-
- if (!waypoints.empty())
+ context->set_location(starts);
+ }
+ else
+ {
+ if (context->debug_positions)
{
- const auto p_final =
- graph.get_waypoint(waypoints.back()).get_location();
- const auto distance = (p_final - p).norm();
- double rotation = 0.0;
- if (final_orientation.has_value())
+ std::stringstream ss;
+ ss << __FILE__ << "|" << __LINE__ << ": setting robot to LOST | "
+ << map << " <" << location.block<2, 1>(0, 0).transpose()
+ << "> orientation " << location[2] * 180.0 / M_PI << "\n";
+ ss << waypoints.size() << " waypoints:";
+ for (std::size_t wp : waypoints)
{
- rotation =
- std::fabs(rmf_utils::wrap_to_pi(location[2] - *final_orientation));
- const auto reversible =
- planner->get_configuration().vehicle_traits()
- .get_differential()->is_reversible();
- if (reversible)
- {
- const double alternate_orientation = rmf_utils::wrap_to_pi(
- *final_orientation + M_PI);
-
- const double alternate_rotation = std::fabs(
- rmf_utils::wrap_to_pi(location[2] - alternate_orientation));
- rotation = std::min(rotation, alternate_rotation);
- }
+ ss << "\n -- " << print_waypoint(wp, graph);
+ }
+ ss << lanes.size() << " lanes:";
+ for (std::size_t lane : lanes)
+ {
+ ss << "\n -- " << print_lane(lane, graph);
}
- const auto& traits = planner->get_configuration().vehicle_traits();
- const auto v = std::max(traits.linear().get_nominal_velocity(), 0.001);
- const auto w =
- std::max(traits.rotational().get_nominal_velocity(), 0.001);
- const auto t = distance / v + rotation / w;
- arrival_estimator(
- rmf_traffic::time::from_seconds(t) + planned_wait_time);
+ std::cout << ss.str() << std::endl;
}
+ context->set_lost(Location { now, map, location });
}
- };
- using DataPtr = std::shared_ptr;
-
- std::weak_ptr w_context;
- std::shared_ptr data;
- std::function begin;
- std::function finisher;
- ActivityIdentifierPtr identifier;
- void finish()
- {
- if (auto context = w_context.lock())
+ if (target_location.has_value())
{
- context->worker().schedule(
- [
- context = context,
- data = this->data,
- identifier = this->identifier,
- finisher = this->finisher
- ](const auto&)
+ const auto p_final = *target_location;
+ const auto distance = (p_final - p).norm();
+ double rotation = 0.0;
+ if (final_orientation.has_value())
+ {
+ rotation =
+ std::fabs(rmf_utils::wrap_to_pi(location[2] - *final_orientation));
+ const auto reversible =
+ planner->get_configuration().vehicle_traits()
+ .get_differential()->is_reversible();
+ if (reversible)
{
- if (!ActivityIdentifier::Implementation::get(*identifier).update_fn)
- {
- // This activity has already finished
- return;
- }
+ const double alternate_orientation = rmf_utils::wrap_to_pi(
+ *final_orientation + M_PI);
- // Prevent this activity from doing any further updates
- ActivityIdentifier::Implementation::get(
- *identifier).update_fn = nullptr;
- if (data->schedule_override.has_value())
- {
- data->release_stubbornness();
- RCLCPP_INFO(
- context->node()->get_logger(),
- "Requesting replan for [%s] after finishing a schedule override",
- context->requester_id().c_str());
- context->request_replan();
- }
- else
- {
- // Trigger the next step in the sequence
- finisher();
- }
- });
+ const double alternate_rotation = std::fabs(
+ rmf_utils::wrap_to_pi(location[2] - alternate_orientation));
+ rotation = std::min(rotation, alternate_rotation);
+ }
+ }
+
+ const auto& traits = planner->get_configuration().vehicle_traits();
+ const auto v = std::max(traits.linear().get_nominal_velocity(), 0.001);
+ const auto w =
+ std::max(traits.rotational().get_nominal_velocity(), 0.001);
+ const auto t = distance / v + rotation / w;
+ arrival_estimator(
+ rmf_traffic::time::from_seconds(t) + planned_wait_time);
}
}
+};
- Stubbornness override_schedule(
- std::string map,
- std::vector path,
- rmf_traffic::Duration hold)
+void EasyFullControl::CommandExecution::Implementation::finish()
+{
+ if (auto context = w_context.lock())
{
- auto stubborn = std::make_shared();
- if (const auto context = w_context.lock())
- {
- context->worker().schedule(
- [
- context,
- stubborn,
- data = this->data,
- identifier = this->identifier,
- map = std::move(map),
- path = std::move(path),
- hold
- ](const auto&)
+ context->worker().schedule(
+ [
+ context = context,
+ data = this->data,
+ identifier = this->identifier,
+ finisher = this->finisher
+ ](const auto&)
+ {
+ if (!ActivityIdentifier::Implementation::get(*identifier).update_fn)
{
- if (!ActivityIdentifier::Implementation::get(*identifier).update_fn)
- {
- // Don't do anything because this command is finished
- return;
- }
+ // This activity has already finished
+ return;
+ }
+ // Prevent this activity from doing any further updates
+ ActivityIdentifier::Implementation::get(
+ *identifier).update_fn = nullptr;
+ if (data && data->schedule_override.has_value())
+ {
data->release_stubbornness();
- data->schedule_override = ScheduleOverride::make(
- context, map, path, hold, stubborn);
- });
- }
-
- return Stubbornness::Implementation::make(stubborn);
+ RCLCPP_INFO(
+ context->node()->get_logger(),
+ "Requesting replan for [%s] after finishing a schedule override",
+ context->requester_id().c_str());
+ context->request_replan();
+ }
+ else
+ {
+ // Trigger the next step in the sequence
+ finisher();
+ }
+ });
}
+}
- static CommandExecution make(
- const std::shared_ptr& context,
- Data data_,
- std::function begin)
+auto EasyFullControl::CommandExecution::Implementation::override_schedule(
+ std::string map,
+ std::vector path,
+ rmf_traffic::Duration hold) -> Stubbornness
+{
+ auto stubborn = std::make_shared();
+ if (const auto context = w_context.lock())
{
- auto data = std::make_shared(data_);
- auto update_fn = [w_context = context->weak_from_this(), data](
- const std::string& map,
- Eigen::Vector3d location)
+ context->worker().schedule(
+ [
+ context,
+ stubborn,
+ data = this->data,
+ identifier = this->identifier,
+ map = std::move(map),
+ path = std::move(path),
+ hold
+ ](const auto&)
{
- if (auto context = w_context.lock())
+ if (!ActivityIdentifier::Implementation::get(*identifier).update_fn)
{
- data->update_location(context, map, location);
+ // Don't do anything because this command is finished
+ return;
}
- };
- auto identifier = ActivityIdentifier::Implementation::make(update_fn);
- CommandExecution cmd;
- cmd._pimpl = rmf_utils::make_impl(
- Implementation{context, data, begin, nullptr, identifier});
- return cmd;
+ data->release_stubbornness();
+ data->schedule_override = ScheduleOverride::make(
+ context, map, path, hold, stubborn);
+ });
}
- static Implementation& get(CommandExecution& cmd)
- {
- return *cmd._pimpl;
- }
-};
+ return Stubbornness::Implementation::make(stubborn);
+}
+
+auto EasyFullControl::CommandExecution::Implementation::make(
+ const std::shared_ptr& context,
+ Data data_,
+ std::function begin) -> CommandExecution
+{
+ auto data = std::make_shared(data_);
+ auto update_fn = [w_context = context->weak_from_this(), data](
+ const std::string& map,
+ Eigen::Vector3d location)
+ {
+ if (auto context = w_context.lock())
+ {
+ data->update_location(context, map, location);
+ }
+ };
+ auto identifier = ActivityIdentifier::Implementation::make(update_fn);
+
+ CommandExecution cmd;
+ cmd._pimpl = rmf_utils::make_impl(
+ Implementation{context, data, begin, nullptr, identifier});
+ return cmd;
+}
+
+auto EasyFullControl::CommandExecution::Implementation::make_hold(
+ const std::shared_ptr& context,
+ rmf_traffic::Time expected_time,
+ rmf_traffic::PlanId plan_id,
+ std::function finisher) -> CommandExecution
+{
+ auto update_fn = [
+ w_context = context->weak_from_this(),
+ expected_time,
+ plan_id
+ ](
+ const std::string& map,
+ Eigen::Vector3d location)
+ {
+ const auto context = w_context.lock();
+ if (!context)
+ return;
+
+ const auto delay = context->now() - expected_time;
+ context->itinerary().cumulative_delay(plan_id, delay);
+ if (const auto nav_params = context->nav_params())
+ {
+ if (context->debug_positions)
+ {
+ std::cout << "Searching for location from " << __FILE__ << "|" <<
+ __LINE__ << std::endl;
+ }
+ nav_params->search_for_location(map, location, *context);
+ }
+ };
+ auto identifier = ActivityIdentifier::Implementation::make(update_fn);
+
+ CommandExecution cmd;
+ cmd._pimpl = rmf_utils::make_impl(
+ Implementation{context, nullptr, nullptr, finisher, identifier});
+ return cmd;
+}
//==============================================================================
void EasyFullControl::CommandExecution::finished()
@@ -631,26 +709,6 @@ EasyFullControl::EasyFullControl()
// Do nothing
}
-//==============================================================================
-class EasyFullControl::Destination::Implementation
-{
-public:
- std::string map;
- Eigen::Vector3d position;
- std::optional graph_index;
- std::optional speed_limit;
- std::optional dock = std::nullopt;
-
- template
- static Destination make(Args&&... args)
- {
- Destination output;
- output._pimpl = rmf_utils::make_impl(
- Implementation{std::forward(args)...});
- return output;
- }
-};
-
//==============================================================================
const std::string& EasyFullControl::Destination::map() const
{
@@ -681,6 +739,12 @@ std::optional EasyFullControl::Destination::graph_index() const
return _pimpl->graph_index;
}
+//==============================================================================
+std::string EasyFullControl::Destination::name() const
+{
+ return _pimpl->name;
+}
+
//==============================================================================
std::optional EasyFullControl::Destination::speed_limit() const
{
@@ -693,6 +757,13 @@ std::optional EasyFullControl::Destination::dock() const
return _pimpl->dock;
}
+//==============================================================================
+rmf_traffic::agv::Graph::LiftPropertiesPtr
+EasyFullControl::Destination::inside_lift() const
+{
+ return _pimpl->lift;
+}
+
//==============================================================================
EasyFullControl::Destination::Destination()
{
@@ -730,6 +801,8 @@ struct ProgressTracker : std::enable_shared_from_this
progress->next();
}
};
+
+
const auto begin = current_activity_impl.begin;
if (begin)
{
@@ -794,28 +867,22 @@ class EasyCommandHandle : public RobotCommandHandle,
const std::string& map,
Eigen::Vector3d position) const
{
- if (!nav_params->transforms_to_robot_coords)
+ auto robot_position = nav_params->to_robot_coordinates(map, position);
+ if (robot_position.has_value())
{
- return position;
+ return *robot_position;
}
- const auto tf_it = nav_params->transforms_to_robot_coords->find(map);
- if (tf_it == nav_params->transforms_to_robot_coords->end())
+ if (const auto context = w_context.lock())
{
- const auto context = w_context.lock();
- if (context)
- {
- RCLCPP_WARN(
- context->node()->get_logger(),
- "[EasyFullControl] Unable to find robot transform for map [%s] for "
- "robot [%s]. We will not apply a transform.",
- map.c_str(),
- context->requester_id().c_str());
- }
- return position;
+ RCLCPP_WARN(
+ context->node()->get_logger(),
+ "[EasyFullControl] Unable to find robot transform for map [%s] for "
+ "robot [%s]. We will not apply a transform.",
+ map.c_str(),
+ context->requester_id().c_str());
}
-
- return tf_it->second.apply(position);
+ return position;
}
std::weak_ptr w_context;
@@ -863,7 +930,7 @@ void EasyCommandHandle::stop()
//==============================================================================
void EasyCommandHandle::follow_new_path(
- const std::vector& waypoints,
+ const std::vector& cmd_waypoints,
ArrivalEstimator next_arrival_estimator_,
RequestCompleted path_finished_callback_)
{
@@ -879,7 +946,7 @@ void EasyCommandHandle::follow_new_path(
context->requester_id().c_str(),
context->itinerary().current_plan_id());
- if (waypoints.empty() ||
+ if (cmd_waypoints.empty() ||
next_arrival_estimator_ == nullptr ||
path_finished_callback_ == nullptr)
{
@@ -901,6 +968,18 @@ void EasyCommandHandle::follow_new_path(
return;
}
const auto& graph = planner->get_configuration().graph();
+ std::vector waypoints = cmd_waypoints;
+
+ if (waypoints.size() < 2)
+ {
+ // This command doesn't actually want us to go anywhere so we will consider
+ // it completed right away. But in case the robot is doing something else
+ // right now, we will command it to stop.
+ stop();
+ path_finished_callback_();
+ return;
+ }
+
std::optional opt_initial_map;
for (const auto& wp : waypoints)
{
@@ -946,7 +1025,8 @@ void EasyCommandHandle::follow_new_path(
{
for (const auto& l : current_location)
{
- if (*wp.graph_index() == l.waypoint() && !l.lane().has_value())
+ if (nav_params->in_same_stack(*wp.graph_index(),
+ l.waypoint()) && !l.lane().has_value())
{
if (l.location().has_value())
{
@@ -978,16 +1058,16 @@ void EasyCommandHandle::follow_new_path(
{
for (const auto& l : current_location)
{
- Eigen::Vector2d p_wp;
+ Eigen::Vector2d p_l;
if (l.location().has_value())
{
- p_wp = l.location()->block<2, 1>(0, 0);
+ p_l = *l.location();
}
else
{
- p_wp = graph.get_waypoint(l.waypoint()).get_location();
+ p_l = graph.get_waypoint(l.waypoint()).get_location();
}
- const double dist = (*l.location() - p_wp).norm();
+ const double dist = (wp.position().block<2, 1>(0, 0) - p_l).norm();
if (dist <= nav_params->max_merge_lane_distance)
{
found_connection = true;
@@ -1094,44 +1174,70 @@ void EasyCommandHandle::follow_new_path(
while (i2 < waypoints.size())
{
const auto& wp2 = waypoints[i2];
- if (wp1.graph_index().has_value() && wp2.graph_index().has_value())
+
+ const bool midlane_wait =
+ !wp1.graph_index().has_value() && !wp2.graph_index().has_value();
+ const bool same_stack =
+ wp1.graph_index().has_value() && wp2.graph_index().has_value()
+ && nav_params->in_same_stack(*wp1.graph_index(), *wp2.graph_index());
+
+ if (same_stack || midlane_wait)
{
- if (*wp1.graph_index() == *wp2.graph_index())
+ target_index = i2;
+ target_position = wp2.position();
+ if (std::abs(wp1.position()[2] -
+ wp2.position()[2])*180.0 / M_PI < 1e-2)
{
- target_index = i2;
- target_position = wp2.position();
- if (std::abs(wp1.position()[2] -
- wp2.position()[2])*180.0 / M_PI < 1e-2)
- {
- // The plan had a wait between these points.
- planned_wait_time += wp2.time() - wp1.time();
- }
- ++i2;
- continue;
+ // The plan had a wait between these points.
+ planned_wait_time += wp2.time() - wp1.time();
}
+ ++i2;
+ continue;
}
break;
}
}
+ rmf_traffic::agv::Graph::LiftPropertiesPtr in_lift;
+ if (wp1.graph_index().has_value())
+ {
+ in_lift = graph.get_waypoint(*wp1.graph_index()).in_lift();
+ }
+ else
+ {
+ for (const auto& lift : graph.all_known_lifts())
+ {
+ if (lift->is_in_lift(target_position.block<2, 1>(0, 0)))
+ {
+ in_lift = lift;
+ break;
+ }
+ }
+ }
+
const auto command_position = to_robot_coordinates(map, target_position);
auto destination = EasyFullControl::Destination::Implementation::make(
std::move(map),
command_position,
wp1.graph_index(),
- speed_limit);
+ nav_params->get_vertex_name(graph, wp1.graph_index()),
+ speed_limit,
+ in_lift);
+ const auto target_p = waypoints.at(target_index).position();
queue.push_back(
EasyFullControl::CommandExecution::Implementation::make(
context,
EasyFullControl::CommandExecution::Implementation::Data{
cmd_wps,
cmd_lanes,
+ target_position.block<2, 1>(0, 0),
target_position[2],
planned_wait_time,
std::nullopt,
nav_params,
- [next_arrival_estimator_, target_index](rmf_traffic::Duration dt)
+ [next_arrival_estimator_, target_index,
+ target_p](rmf_traffic::Duration dt)
{
next_arrival_estimator_(target_index, dt);
}
@@ -1260,13 +1366,17 @@ void EasyCommandHandle::dock(
const double dist = (p1 - p0).norm();
const auto& traits = planner->get_configuration().vehicle_traits();
const double v = std::max(traits.linear().get_nominal_velocity(), 0.001);
- const double dt = dist / v;
- const rmf_traffic::Time expected_arrival =
- context->now() + rmf_traffic::time::from_seconds(dt);
+ const auto dt = rmf_traffic::time::from_seconds(dist / v);
+ const auto& itin = context->itinerary();
+ const auto now = context->now();
+ const auto initial_delay = itin.cumulative_delay(itin.current_plan_id())
+ .value_or(rmf_traffic::Duration(0));
+ const rmf_traffic::Time expected_arrival = now + dt - initial_delay;
auto data = EasyFullControl::CommandExecution::Implementation::Data{
{i0, i1},
{*found_lane},
+ p1,
std::nullopt,
rmf_traffic::Duration(0),
std::nullopt,
@@ -1280,11 +1390,23 @@ void EasyCommandHandle::dock(
return;
}
- const rmf_traffic::Time now = context->now();
- const auto updated_arrival = now + dt;
- const auto delay = updated_arrival - expected_arrival;
- context->itinerary().cumulative_delay(
- plan_id, delay, std::chrono::seconds(1));
+ context->worker().schedule([
+ w = context->weak_from_this(),
+ expected_arrival,
+ plan_id,
+ dt
+ ](const auto&)
+ {
+ const auto context = w.lock();
+ if (!context)
+ return;
+
+ const rmf_traffic::Time now = context->now();
+ const auto updated_arrival = now + dt;
+ const auto delay = updated_arrival - expected_arrival;
+ context->itinerary().cumulative_delay(
+ plan_id, delay, std::chrono::seconds(1));
+ });
}
};
@@ -1304,7 +1426,9 @@ void EasyCommandHandle::dock(
wp1.get_map_name(),
command_position,
i1,
+ nav_params->get_vertex_name(graph, i1),
lane.properties().speed_limit(),
+ wp1.in_lift(),
dock_name_);
auto cmd = EasyFullControl::CommandExecution::Implementation::make(
@@ -1318,6 +1442,7 @@ void EasyCommandHandle::dock(
{
handle_nav_request(destination, execution);
});
+
this->current_progress = ProgressTracker::make(
{std::move(cmd)},
std::move(docking_finished_callback_));
@@ -1431,33 +1556,17 @@ void EasyFullControl::EasyRobotUpdateHandle::update(
ActivityIdentifier::Implementation::get(*current_activity).update_fn;
if (update_fn)
{
- update_fn(state.map(), position);
+ update_fn(
+ state.map(), position);
return;
}
}
- auto planner = context->planner();
- if (!planner)
+ if (context->debug_positions)
{
- RCLCPP_ERROR(
- context->node()->get_logger(),
- "Planner unavailable for robot [%s], cannot update its location",
- context->requester_id().c_str());
- return;
- }
- const auto& graph = planner->get_configuration().graph();
- const auto& nav_params = updater->nav_params;
- const auto now = context->now();
- auto starts =
- nav_params->compute_plan_starts(graph, state.map(), position, now);
- if (!starts.empty())
- {
- context->set_location(std::move(starts));
- }
- else
- {
- context->set_lost(Location { now, state.map(), position });
+ std::cout << "Searching for location from " << __FILE__ << "|" << __LINE__ << std::endl;
}
+ updater->nav_params->search_for_location(state.map(), position, *context);
});
}
@@ -1559,6 +1668,7 @@ class EasyFullControl::FleetConfiguration::Implementation
double default_max_merge_waypoint_distance;
double default_max_merge_lane_distance;
double default_min_lane_length;
+ std::unordered_map lift_emergency_levels;
};
//==============================================================================
@@ -1612,7 +1722,8 @@ EasyFullControl::FleetConfiguration::FleetConfiguration(
default_responsive_wait,
std::move(default_max_merge_waypoint_distance),
std::move(default_max_merge_lane_distance),
- std::move(default_min_lane_length)
+ std::move(default_min_lane_length),
+ {}
}))
{
// Do nothing
@@ -1893,8 +2004,10 @@ EasyFullControl::FleetConfiguration::from_config_files(
rmf_task::ConstRequestFactoryPtr finishing_request;
if (finishing_request_string == "charge")
{
- finishing_request =
+ auto charge_factory =
std::make_shared();
+ charge_factory->set_indefinite(true);
+ finishing_request = charge_factory;
std::cout
<< "Fleet is configured to perform ChargeBattery as finishing request"
<< std::endl;
@@ -1902,7 +2015,8 @@ EasyFullControl::FleetConfiguration::from_config_files(
else if (finishing_request_string == "park")
{
finishing_request =
- std::make_shared();
+ std::make_shared(
+ "idle", nullptr);
std::cout
<< "Fleet is configured to perform ParkRobot as finishing request"
<< std::endl;
@@ -2060,7 +2174,7 @@ EasyFullControl::FleetConfiguration::from_config_files(
const auto robot_name = robot.first.as();
const YAML::Node& robot_config_yaml = robot.second;
- if (!robot_config_yaml.IsMap())
+ if (!robot_config_yaml.IsMap() && !robot_config_yaml.IsNull())
{
std::cerr
<< "Entry for [" << robot_name << "] in [robots] dictionary is not "
@@ -2068,65 +2182,90 @@ EasyFullControl::FleetConfiguration::from_config_files(
return std::nullopt;
}
- std::string charger;
- const YAML::Node& charger_yaml = robot_config_yaml["charger"];
- if (charger_yaml)
- {
- charger = charger_yaml.as();
- }
- else
+ if (robot_config_yaml.IsMap())
{
- std::cerr
- << "No [charger] listed for [" << robot_name << "] in the config "
- << "file [" << config_file << "]. A robot configuration cannot be "
- << "made for the robot." << std::endl;
- return std::nullopt;
- }
+ std::vector chargers;
+ const YAML::Node& charger_yaml = robot_config_yaml["charger"];
+ if (charger_yaml)
+ {
+ chargers.push_back(charger_yaml.as());
+ }
- const YAML::Node& responsive_wait_yaml =
- robot_config_yaml["responsive_wait"];
- std::optional responsive_wait = std::nullopt;
- if (responsive_wait_yaml)
- {
- responsive_wait = responsive_wait_yaml.as();
- }
+ const YAML::Node& responsive_wait_yaml =
+ robot_config_yaml["responsive_wait"];
+ std::optional responsive_wait = std::nullopt;
+ if (responsive_wait_yaml)
+ {
+ responsive_wait = responsive_wait_yaml.as();
+ }
- const YAML::Node& max_merge_waypoint_distance_yaml =
- robot_config_yaml["max_merge_waypoint_distance"];
- std::optional max_merge_waypoint_distance = std::nullopt;
- if (max_merge_waypoint_distance_yaml)
- {
- max_merge_waypoint_distance =
- max_merge_waypoint_distance_yaml.as();
- }
+ const YAML::Node& max_merge_waypoint_distance_yaml =
+ robot_config_yaml["max_merge_waypoint_distance"];
+ std::optional max_merge_waypoint_distance = std::nullopt;
+ if (max_merge_waypoint_distance_yaml)
+ {
+ max_merge_waypoint_distance =
+ max_merge_waypoint_distance_yaml.as();
+ }
- const YAML::Node& max_merge_lane_distance_yaml =
- robot_config_yaml["max_merge_lane_distance"];
- std::optional max_merge_lane_distance = std::nullopt;
- if (max_merge_lane_distance_yaml)
- {
- max_merge_lane_distance = max_merge_lane_distance_yaml.as();
- }
+ const YAML::Node& max_merge_lane_distance_yaml =
+ robot_config_yaml["max_merge_lane_distance"];
+ std::optional max_merge_lane_distance = std::nullopt;
+ if (max_merge_lane_distance_yaml)
+ {
+ max_merge_lane_distance = max_merge_lane_distance_yaml.as();
+ }
- const YAML::Node& min_lane_length_yaml =
- robot_config_yaml["min_lane_length"];
- std::optional min_lane_length = std::nullopt;
- if (min_lane_length_yaml)
- {
- min_lane_length = min_lane_length_yaml.as();
- }
+ const YAML::Node& min_lane_length_yaml =
+ robot_config_yaml["min_lane_length"];
+ std::optional min_lane_length = std::nullopt;
+ if (min_lane_length_yaml)
+ {
+ min_lane_length = min_lane_length_yaml.as();
+ }
- auto config = RobotConfiguration({std::move(charger)},
+ auto config = RobotConfiguration(
+ std::move(chargers),
responsive_wait,
max_merge_waypoint_distance,
max_merge_lane_distance,
min_lane_length);
- known_robot_configurations.insert_or_assign(robot_name, config);
+ known_robot_configurations.insert_or_assign(robot_name, config);
+ }
+ else
+ {
+ auto config = RobotConfiguration({});
+ known_robot_configurations.insert_or_assign(robot_name, config);
+ }
}
}
}
- return FleetConfiguration(
+ std::unordered_map lift_emergency_levels;
+ const YAML::Node& lift_emergency_levels_yaml =
+ rmf_fleet["lift_emergency_levels"];
+ if (lift_emergency_levels_yaml)
+ {
+ if (!lift_emergency_levels_yaml.IsMap())
+ {
+ std::cerr
+ << "[lift_emergency_levels] element is not a map in the config file ["
+ << config_file << "] so we cannot parse what level each lift will go "
+ << "to in an emergency." << std::endl;
+ return std::nullopt;
+ }
+ else
+ {
+ for (const auto& lift : lift_emergency_levels_yaml)
+ {
+ auto lift_name = lift.first.as();
+ auto level_name = lift.second.as();
+ lift_emergency_levels[std::move(lift_name)] = std::move(level_name);
+ }
+ }
+ }
+
+ auto config = FleetConfiguration(
fleet_name,
std::move(tf_dict),
std::move(known_robot_configurations),
@@ -2150,6 +2289,8 @@ EasyFullControl::FleetConfiguration::from_config_files(
default_max_merge_waypoint_distance,
default_max_merge_lane_distance,
default_min_lane_length);
+ config.change_lift_emergency_levels() = lift_emergency_levels;
+ return config;
}
//==============================================================================
@@ -2495,6 +2636,29 @@ void EasyFullControl::FleetConfiguration::set_default_min_lane_length(
_pimpl->default_min_lane_length = distance;
}
+//==============================================================================
+void EasyFullControl::FleetConfiguration::set_lift_emergency_level(
+ std::string lift_name,
+ std::string emergency_level_name)
+{
+ _pimpl->lift_emergency_levels[std::move(lift_name)] =
+ std::move(emergency_level_name);
+}
+
+//==============================================================================
+std::unordered_map&
+EasyFullControl::FleetConfiguration::change_lift_emergency_levels()
+{
+ return _pimpl->lift_emergency_levels;
+}
+
+//==============================================================================
+const std::unordered_map&
+EasyFullControl::FleetConfiguration::lift_emergency_levels() const
+{
+ return _pimpl->lift_emergency_levels;
+}
+
//==============================================================================
using EasyCommandHandlePtr = std::shared_ptr;
@@ -2525,6 +2689,28 @@ auto EasyFullControl::add_robot(
auto easy_updater = EasyRobotUpdateHandle::Implementation::make(
robot_nav_params, worker);
+ LocalizationRequest localization = nullptr;
+ if (callbacks.localize())
+ {
+ localization = [
+ inner = callbacks.localize(),
+ nav_params = robot_nav_params
+ ](Destination estimate, CommandExecution execution)
+ {
+ auto robot_position = nav_params->to_robot_coordinates(
+ estimate.map(),
+ estimate.position());
+ if (robot_position.has_value())
+ {
+ auto transformed_estimate = estimate;
+ Destination::Implementation::get(transformed_estimate)
+ .position = *robot_position;
+
+ inner(transformed_estimate, execution);
+ }
+ };
+ }
+
const auto& fleet_impl =
FleetUpdateHandle::Implementation::get(*_pimpl->fleet_handle);
const auto& planner = *fleet_impl.planner;
@@ -2549,36 +2735,39 @@ auto EasyFullControl::add_robot(
return nullptr;
}
- if (configuration.compatible_chargers().size() != 1)
+ if (configuration.compatible_chargers().size() > 1)
{
- RCLCPP_ERROR(
+ RCLCPP_WARN(
node->get_logger(),
- "Robot [%s] is configured to have %lu chargers, but we require it to "
- "have exactly 1. It will not be added to the fleet.",
+ "Robot [%s] is configured to have %lu chargers, but we will only make "
+ "use of the first one. Making use of multiple chargers will be added in "
+ "a future version of RMF.",
robot_name.c_str(),
configuration.compatible_chargers().size());
-
- _pimpl->cmd_handles.erase(robot_name);
- return nullptr;
}
- const auto& charger_name = configuration.compatible_chargers().front();
- const auto* charger_wp = graph.find_waypoint(charger_name);
- if (!charger_wp)
+ std::optional charger_index;
+ if (!configuration.compatible_chargers().empty())
{
- RCLCPP_ERROR(
- node->get_logger(),
- "Cannot find a waypoint named [%s] in the navigation graph of fleet [%s] "
- "needed for the charging point of robot [%s]. We will not add the robot "
- "to the fleet.",
- charger_name.c_str(),
- fleet_name.c_str(),
- robot_name.c_str());
+ const auto& charger_name = configuration.compatible_chargers().front();
+ const auto* charger_wp = graph.find_waypoint(charger_name);
+ if (!charger_wp)
+ {
+ RCLCPP_ERROR(
+ node->get_logger(),
+ "Cannot find a waypoint named [%s] in the navigation graph of fleet "
+ "[%s] needed for the charging point of robot [%s]. We will not add the "
+ "robot to the fleet.",
+ charger_name.c_str(),
+ fleet_name.c_str(),
+ robot_name.c_str());
+
+ _pimpl->cmd_handles.erase(robot_name);
+ return nullptr;
+ }
- _pimpl->cmd_handles.erase(robot_name);
- return nullptr;
+ charger_index = charger_wp->index();
}
- const std::size_t charger_index = charger_wp->index();
rmf_traffic::Time now = std::chrono::steady_clock::time_point(
std::chrono::nanoseconds(node->now().nanoseconds()));
@@ -2614,6 +2803,7 @@ auto EasyFullControl::add_robot(
robot_nav_params->min_lane_length = *configuration.min_lane_length();
}
+ robot_nav_params->find_stacked_vertices(graph);
const Eigen::Vector3d position = *position_opt;
auto starts = robot_nav_params->compute_plan_starts(
graph, initial_state.map(), position, now);
@@ -2679,6 +2869,7 @@ auto EasyFullControl::add_robot(
fleet_name = fleet_name,
charger_index,
action_executor = callbacks.action_executor(),
+ localization = std::move(localization),
nav_params = robot_nav_params,
enable_responsive_wait
](const RobotUpdateHandlePtr& updater)
@@ -2696,6 +2887,7 @@ auto EasyFullControl::add_robot(
fleet_name,
charger_index,
action_executor,
+ localization,
context,
nav_params,
enable_responsive_wait
@@ -2706,7 +2898,11 @@ auto EasyFullControl::add_robot(
EasyRobotUpdateHandle::Implementation::get(*easy_updater)
.updater->handle = handle;
handle->set_action_executor(action_executor);
- handle->set_charger_waypoint(charger_index);
+ context->set_localization(localization);
+ if (charger_index.has_value())
+ {
+ handle->set_charger_waypoint(*charger_index);
+ }
handle->enable_responsive_wait(enable_responsive_wait);
RCLCPP_INFO(
diff --git a/rmf_fleet_adapter/src/rmf_fleet_adapter/agv/FleetUpdateHandle.cpp b/rmf_fleet_adapter/src/rmf_fleet_adapter/agv/FleetUpdateHandle.cpp
index 61365bf03..2e148d22d 100644
--- a/rmf_fleet_adapter/src/rmf_fleet_adapter/agv/FleetUpdateHandle.cpp
+++ b/rmf_fleet_adapter/src/rmf_fleet_adapter/agv/FleetUpdateHandle.cpp
@@ -169,18 +169,6 @@ void FleetUpdateHandle::Implementation::dock_summary_cb(
return;
}
-//==============================================================================
-std::string FleetUpdateHandle::Implementation::make_error_str(
- uint64_t code, std::string category, std::string detail) const
-{
- nlohmann::json error;
- error["code"] = code;
- error["category"] = std::move(category);
- error["detail"] = std::move(detail);
-
- return error.dump();
-}
-
//==============================================================================
std::shared_ptr FleetUpdateHandle::Implementation::convert(
const std::string& task_id,
@@ -315,6 +303,131 @@ std::shared_ptr FleetUpdateHandle::Implementation::convert(
return new_request;
}
+//==============================================================================
+class AllocateTasks : public std::enable_shared_from_this
+{
+public:
+
+ template
+ void operator()(const Subscriber& s)
+ {
+ std::vector errors;
+ auto assignments = run(errors);
+ s.on_next(Result{std::move(assignments), std::move(errors)});
+ s.on_completed();
+ }
+
+ std::optional run(std::vector& errors)
+ {
+ std::string id = "";
+
+ if (new_request)
+ {
+ expect.pending_requests.push_back(new_request);
+ id = new_request->booking()->id();
+ }
+
+ RCLCPP_INFO(
+ node->get_logger(),
+ "Planning for [%ld] robot(s) and [%ld] request(s)",
+ expect.states.size(),
+ expect.pending_requests.size());
+
+ // Generate new task assignments
+ const auto result = task_planner.plan(
+ rmf_traffic_ros2::convert(node->now()),
+ expect.states,
+ expect.pending_requests);
+
+ auto assignments_ptr = std::get_if<
+ rmf_task::TaskPlanner::Assignments>(&result);
+
+ if (!assignments_ptr)
+ {
+ auto error = std::get_if<
+ rmf_task::TaskPlanner::TaskPlannerError>(&result);
+
+ if (*error == rmf_task::TaskPlanner::TaskPlannerError::low_battery)
+ {
+ std::string error_str =
+ "[TaskPlanner] Failed to compute assignments for task_id [" + id
+ +
+ "] due to insufficient initial battery charge for all robots in this "
+ "fleet.";
+
+ RCLCPP_ERROR(node->get_logger(), "%s", error_str.c_str());
+ errors.push_back(
+ make_error_str(9, "Not feasible", std::move(error_str)));
+ }
+
+ else if (*error ==
+ rmf_task::TaskPlanner::TaskPlannerError::limited_capacity)
+ {
+ std::string error_str =
+ "[TaskPlanner] Failed to compute assignments for task_id [" + id
+ + "] due to insufficient battery capacity to accommodate one or more "
+ "requests by any of the robots in this fleet.";
+
+ RCLCPP_ERROR(node->get_logger(), "%s", error_str.c_str());
+ errors.push_back(
+ make_error_str(9, "Not feasible", std::move(error_str)));
+ }
+
+ else
+ {
+ std::string error_str =
+ "[TaskPlanner] Failed to compute assignments for task_id [" + id +
+ "]";
+
+ RCLCPP_ERROR(node->get_logger(), "%s", error_str.c_str());
+ errors.push_back(
+ make_error_str(9, "Not feasible", std::move(error_str)));
+ }
+
+ return std::nullopt;
+ }
+
+ const auto assignments = *assignments_ptr;
+
+ if (assignments.empty())
+ {
+ RCLCPP_ERROR(
+ node->get_logger(),
+ "[TaskPlanner] Failed to compute assignments for task_id [%s]",
+ id.c_str());
+
+ return std::nullopt;
+ }
+
+ return assignments;
+ }
+
+ struct Result
+ {
+ std::optional assignments;
+ std::vector errors;
+ };
+
+ AllocateTasks(
+ rmf_task::ConstRequestPtr new_request_,
+ Expectations expect_,
+ rmf_task::TaskPlanner task_planner_,
+ std::shared_ptr node_)
+ : new_request(std::move(new_request_)),
+ expect(std::move(expect_)),
+ task_planner(std::move(task_planner_)),
+ node(std::move(node_))
+ {
+ // Do nothing
+ }
+
+ rmf_task::ConstRequestPtr new_request;
+ Expectations expect;
+ rmf_task::TaskPlanner task_planner;
+ std::shared_ptr node;
+};
+
+
//==============================================================================
void FleetUpdateHandle::Implementation::bid_notice_cb(
const BidNoticeMsg& bid_notice,
@@ -358,7 +471,8 @@ void FleetUpdateHandle::Implementation::bid_notice_cb(
const auto request_msg = nlohmann::json::parse(bid_notice.request);
static const auto request_validator =
- nlohmann::json_schema::json_validator(rmf_api_msgs::schemas::task_request);
+ nlohmann::json_schema::json_validator(
+ rmf_api_msgs::schemas::task_request);
try
{
@@ -390,104 +504,124 @@ void FleetUpdateHandle::Implementation::bid_notice_cb(
});
}
- // TODO(MXG): Make the task planning asynchronous. The worker should schedule
- // a job to perform the planning which should then spawn a job to save the
- // plan result and respond. I started to refactor allocate_tasks(~) to make it
- // async, but I will save the remaining effort for later, when there is more
- // time to spare.
- auto allocation_result = allocate_tasks(new_request, &errors);
- if (!allocation_result.has_value())
- return respond({std::nullopt, std::move(errors)});
+ calculate_bid = std::make_shared(
+ new_request,
+ aggregate_expectations(),
+ *task_planner,
+ node);
- const auto& assignments = allocation_result.value();
+ auto receive_allocation = [w = weak_self, respond, task_id](
+ AllocateTasks::Result result)
+ {
+ const auto self = w.lock();
+ if (!self)
+ return;
- const double cost = task_planner->compute_cost(assignments);
+ auto allocation_result = result.assignments;
+ if (!allocation_result.has_value())
+ return respond({std::nullopt, std::move(result.errors)});
- // Display computed assignments for debugging
- std::stringstream debug_stream;
- debug_stream << "Cost: " << cost << std::endl;
- for (std::size_t i = 0; i < assignments.size(); ++i)
- {
- debug_stream << "--Agent: " << i << std::endl;
- for (const auto& a : assignments[i])
- {
- const auto& s = a.finish_state();
- const double request_seconds =
- a.request()->booking()->earliest_start_time().time_since_epoch().count()
- /1e9;
- const double start_seconds =
- a.deployment_time().time_since_epoch().count()/1e9;
- const rmf_traffic::Time finish_time = s.time().value();
- const double finish_seconds = finish_time.time_since_epoch().count()/1e9;
- debug_stream << " <" << a.request()->booking()->id() << ": " <<
- request_seconds
- << ", " << start_seconds
- << ", "<< finish_seconds << ", " << s.battery_soc().value()
- << "%>" << std::endl;
- }
- }
- debug_stream << " ----------------------" << std::endl;
+ const auto& assignments = allocation_result.value();
- RCLCPP_DEBUG(node->get_logger(), "%s", debug_stream.str().c_str());
+ const double cost = self->_pimpl->task_planner->compute_cost(assignments);
- // Map robot index to name to populate robot_name in BidProposal
- std::unordered_map robot_name_map;
- std::size_t index = 0;
- for (const auto& t : task_managers)
- {
- robot_name_map.insert({index, t.first->name()});
- ++index;
- }
+ // Display computed assignments for debugging
+ std::stringstream debug_stream;
+ debug_stream << "Cost: " << cost << std::endl;
+ for (std::size_t i = 0; i < assignments.size(); ++i)
+ {
+ debug_stream << "--Agent: " << i << std::endl;
+ for (const auto& a : assignments[i])
+ {
+ const auto& s = a.finish_state();
+ const double request_seconds =
+ a.request()->booking()->earliest_start_time().time_since_epoch().
+ count()
+ /1e9;
+ const double start_seconds =
+ a.deployment_time().time_since_epoch().count()/1e9;
+ const rmf_traffic::Time finish_time = s.time().value();
+ const double finish_seconds = finish_time.time_since_epoch().count()/
+ 1e9;
+ debug_stream << " <" << a.request()->booking()->id() << ": " <<
+ request_seconds
+ << ", " << start_seconds
+ << ", "<< finish_seconds << ", " <<
+ s.battery_soc().value()
+ << "%>" << std::endl;
+ }
+ }
+ debug_stream << " ----------------------" << std::endl;
- std::optional robot_name;
- std::optional finish_time;
- index = 0;
- for (const auto& agent : assignments)
- {
- for (const auto& assignment : agent)
- {
- if (assignment.request()->booking()->id() == task_id)
+ RCLCPP_DEBUG(
+ self->_pimpl->node->get_logger(),
+ "%s",
+ debug_stream.str().c_str());
+
+ // Map robot index to name to populate robot_name in BidProposal
+ std::unordered_map robot_name_map;
+ std::size_t index = 0;
+ for (const auto& t : self->_pimpl->task_managers)
{
- finish_time = assignment.finish_state().time().value();
- if (robot_name_map.find(index) != robot_name_map.end())
- robot_name = robot_name_map[index];
- break;
+ robot_name_map.insert({index, t.first->name()});
+ ++index;
}
- }
- ++index;
- }
- if (!robot_name.has_value() || !finish_time.has_value())
- {
- errors.push_back(
- make_error_str(
- 13, "Internal bug",
- "Failed to find robot_name or finish_time after allocating task. "
- "Please report this bug to the RMF developers."));
+ std::optional robot_name;
+ std::optional finish_time;
+ index = 0;
+ for (const auto& agent : assignments)
+ {
+ for (const auto& assignment : agent)
+ {
+ if (assignment.request()->booking()->id() == task_id)
+ {
+ finish_time = assignment.finish_state().time().value();
+ if (robot_name_map.find(index) != robot_name_map.end())
+ robot_name = robot_name_map[index];
+ break;
+ }
+ }
+ ++index;
+ }
- return respond({std::nullopt, std::move(errors)});
- }
+ if (!robot_name.has_value() || !finish_time.has_value())
+ {
+ result.errors.push_back(
+ make_error_str(
+ 13, "Internal bug",
+ "Failed to find robot_name or finish_time after allocating task. "
+ "Please report this bug to the RMF developers."));
- // Publish BidProposal
- respond(
- {
- rmf_task_ros2::bidding::Response::Proposal{
- name,
- *robot_name,
- current_assignment_cost,
- cost,
- *finish_time
- },
- std::move(errors)
- });
+ return respond({std::nullopt, std::move(result.errors)});
+ }
- RCLCPP_INFO(
- node->get_logger(),
- "Submitted BidProposal to accommodate task [%s] by robot [%s] with new cost [%f]",
- task_id.c_str(), robot_name->c_str(), cost);
+ // Publish BidProposal
+ respond(
+ {
+ rmf_task_ros2::bidding::Response::Proposal{
+ self->_pimpl->name,
+ *robot_name,
+ self->_pimpl->current_assignment_cost,
+ cost,
+ *finish_time
+ },
+ std::move(result.errors)
+ });
+
+ RCLCPP_INFO(
+ self->_pimpl->node->get_logger(),
+ "Submitted BidProposal to accommodate task [%s] by robot [%s] with new cost [%f]",
+ task_id.c_str(), robot_name->c_str(), cost);
+
+ // Store assignments in internal map
+ self->_pimpl->bid_notice_assignments.insert({task_id, assignments});
+ };
- // Store assignments in internal map
- bid_notice_assignments.insert({task_id, assignments});
+ calculate_bid_subscription = rmf_rxcpp::make_job(
+ calculate_bid)
+ .observe_on(rxcpp::identity_same_worker(worker))
+ .subscribe(receive_allocation);
}
//==============================================================================
@@ -619,7 +753,10 @@ void FleetUpdateHandle::Implementation::dispatch_command_cb(
// TODO: This replanning is blocking the main thread. Instead, the
// replanning should run on a separate worker and then deliver the
// result back to the main worker.
- const auto replan_results = allocate_tasks(request, &dispatch_ack.errors);
+ const auto replan_results = AllocateTasks(
+ nullptr, aggregate_expectations(), *task_planner, node)
+ .run(dispatch_ack.errors);
+
if (!replan_results)
{
std::string error_str =
@@ -639,22 +776,24 @@ void FleetUpdateHandle::Implementation::dispatch_command_cb(
// rxcpp worker. Hence, no new tasks would have started during this
// replanning.
}
-
- std::size_t index = 0;
- for (auto& t : task_managers)
+ else
{
- t.second->set_queue(assignments[index]);
- ++index;
- }
+ std::size_t index = 0;
+ for (auto& t : task_managers)
+ {
+ t.second->set_queue(assignments[index]);
+ ++index;
+ }
- current_assignment_cost = task_planner->compute_cost(assignments);
- dispatch_ack.success = true;
- dispatch_ack_pub->publish(dispatch_ack);
+ current_assignment_cost = task_planner->compute_cost(assignments);
+ dispatch_ack.success = true;
+ dispatch_ack_pub->publish(dispatch_ack);
- RCLCPP_INFO(
- node->get_logger(),
- "Assignments updated for robots in fleet [%s] to accommodate task_id [%s]",
- name.c_str(), task_id.c_str());
+ RCLCPP_INFO(
+ node->get_logger(),
+ "TaskAssignments updated for robots in fleet [%s] to accommodate task_id [%s]",
+ name.c_str(), task_id.c_str());
+ }
}
else if (msg->type == DispatchCmdMsg::TYPE_REMOVE)
{
@@ -681,7 +820,10 @@ void FleetUpdateHandle::Implementation::dispatch_command_cb(
{
// Re-plan assignments while ignoring request for task to be cancelled
std::vector errors;
- const auto replan_results = allocate_tasks(nullptr, &errors);
+ const auto replan_results = AllocateTasks(
+ nullptr, aggregate_expectations(), *task_planner, node)
+ .run(errors);
+
if (!replan_results.has_value())
{
std::stringstream ss;
@@ -718,7 +860,7 @@ void FleetUpdateHandle::Implementation::dispatch_command_cb(
RCLCPP_INFO(
node->get_logger(),
- "Task with task_id [%s] has successfully been cancelled. Assignments "
+ "Task with task_id [%s] has successfully been cancelled. TaskAssignments "
"updated for robots in fleet [%s].",
task_id.c_str(), name.c_str());
}
@@ -741,7 +883,7 @@ void FleetUpdateHandle::Implementation::dispatch_command_cb(
//==============================================================================
auto FleetUpdateHandle::Implementation::is_valid_assignments(
- Assignments& assignments) const -> bool
+ TaskAssignments& assignments) const -> bool
{
std::unordered_set executed_tasks;
for (const auto& [context, mgr] : task_managers)
@@ -1016,6 +1158,160 @@ void FleetUpdateHandle::Implementation::update_fleet_logs() const
}
}
+//==============================================================================
+void FleetUpdateHandle::Implementation::handle_emergency(
+ const bool is_emergency)
+{
+ if (is_emergency == emergency_active)
+ return;
+
+ emergency_active = is_emergency;
+ if (is_emergency)
+ {
+ update_emergency_planner();
+ }
+
+ for (const auto& [context, _] : task_managers)
+ {
+ context->_set_emergency(is_emergency);
+ }
+ emergency_publisher.get_subscriber().on_next(is_emergency);
+}
+
+//==============================================================================
+namespace {
+class EmergencyLaneCloser : public rmf_traffic::agv::Graph::Lane::Executor
+{
+public:
+ void execute(const DoorOpen&) override {}
+ void execute(const DoorClose&) override {}
+ void execute(const LiftSessionEnd&) override {}
+ void execute(const LiftMove&) override {}
+ void execute(const Wait&) override {}
+ void execute(const Dock& dock) override {}
+ void execute(const LiftSessionBegin& info) override
+ {
+ lift = info.lift_name();
+ enter = true;
+ }
+ void execute(const LiftDoorOpen& info) override
+ {
+ lift = info.lift_name();
+ exit = true;
+ }
+
+ std::string lift;
+ bool enter = false;
+ bool exit = false;
+};
+} // anonymous namespace
+
+//==============================================================================
+std::vector find_emergency_lift_closures(
+ const rmf_traffic::agv::Graph& graph,
+ const std::unordered_map& emergency_level_for_lift)
+{
+ std::vector closures;
+ for (std::size_t i = 0; i < graph.num_lanes(); ++i)
+ {
+ EmergencyLaneCloser executor;
+ const auto& lane = graph.get_lane(i);
+ if (const auto* event = lane.entry().event())
+ event->execute(executor);
+
+ if (const auto* event = lane.exit().event())
+ event->execute(executor);
+
+ const auto wp0 = lane.entry().waypoint_index();
+ const auto wp1 = lane.exit().waypoint_index();
+ if (executor.enter)
+ {
+ if (emergency_level_for_lift.count(executor.lift) > 0)
+ {
+ closures.push_back(i);
+ }
+ }
+ else if (executor.exit)
+ {
+ const auto l_it = emergency_level_for_lift.find(executor.lift);
+ if (l_it != emergency_level_for_lift.end())
+ {
+ const auto& map = graph.get_waypoint(
+ lane.exit().waypoint_index()).get_map_name();
+ if (l_it->second != map)
+ {
+ closures.push_back(i);
+ }
+ }
+ }
+ }
+
+ return closures;
+}
+
+//==============================================================================
+void FleetUpdateHandle::Implementation::update_emergency_planner()
+{
+ const auto& config = (*planner)->get_configuration();
+ const auto lift_closures = find_emergency_lift_closures(
+ config.graph(),
+ emergency_level_for_lift);
+ const auto& normal_closures = config.lane_closures();
+ auto emergency_closures = normal_closures;
+ for (const std::size_t lane : lift_closures)
+ {
+ emergency_closures.close(lane);
+ }
+
+ auto emergency_config = config;
+ emergency_config.lane_closures(std::move(emergency_closures));
+
+ *emergency_planner = std::make_shared(
+ emergency_config, rmf_traffic::agv::Planner::Options(nullptr));
+}
+
+//==============================================================================
+void FleetUpdateHandle::Implementation::update_charging_assignments(
+ const ChargingAssignments& charging)
+{
+ if (charging.fleet_name != name)
+ return;
+
+ RCLCPP_INFO(
+ node->get_logger(),
+ "Fleet [%s] received new charging assignments",
+ name.c_str());
+
+ for (const ChargingAssignment& assignment : charging.assignments)
+ {
+ bool found_robot = false;
+ for (const auto& [context, _] : task_managers)
+ {
+ if (context->name() != assignment.robot_name)
+ continue;
+
+ const rmf_traffic::agv::Graph& graph = context->navigation_graph();
+ const auto wp = graph.find_waypoint(assignment.waypoint_name);
+ if (!wp)
+ {
+ RCLCPP_ERROR(
+ node->get_logger(),
+ "Cannot change charging waypoint for [%s] to [%s] because it does "
+ "not exist in the graph",
+ context->requester_id().c_str(),
+ assignment.waypoint_name.c_str());
+ }
+ const bool wait_for_charger = assignment.mode == assignment.MODE_WAIT;
+ context->_set_charging(wp->index(), wait_for_charger);
+ }
+
+ if (!found_robot)
+ {
+ unregistered_charging_assignments[assignment.robot_name] = assignment;
+ }
+ }
+}
+
//==============================================================================
nlohmann::json_schema::json_validator
FleetUpdateHandle::Implementation::make_validator(
@@ -1197,106 +1493,6 @@ auto FleetUpdateHandle::Implementation::aggregate_expectations() const
return expect;
}
-//==============================================================================
-auto FleetUpdateHandle::Implementation::allocate_tasks(
- rmf_task::ConstRequestPtr new_request,
- std::vector* errors,
- std::optional expectations) const -> std::optional
-{
- // Collate robot states, constraints and combine new requestptr with
- // requestptr of non-charging tasks in task manager queues
- auto expect = expectations.has_value() ? expectations.value() :
- aggregate_expectations();
- std::string id = "";
-
- if (new_request)
- {
- expect.pending_requests.push_back(new_request);
- id = new_request->booking()->id();
- }
-
- RCLCPP_INFO(
- node->get_logger(),
- "Planning for [%ld] robot(s) and [%ld] request(s)",
- expect.states.size(),
- expect.pending_requests.size());
-
- // Generate new task assignments
- const auto result = task_planner->plan(
- rmf_traffic_ros2::convert(node->now()),
- expect.states,
- expect.pending_requests);
-
- auto assignments_ptr = std::get_if<
- rmf_task::TaskPlanner::Assignments>(&result);
-
- if (!assignments_ptr)
- {
- auto error = std::get_if<
- rmf_task::TaskPlanner::TaskPlannerError>(&result);
-
- if (*error == rmf_task::TaskPlanner::TaskPlannerError::low_battery)
- {
- std::string error_str =
- "[TaskPlanner] Failed to compute assignments for task_id [" + id
- + "] due to insufficient initial battery charge for all robots in this "
- "fleet.";
-
- RCLCPP_ERROR(node->get_logger(), "%s", error_str.c_str());
- if (errors)
- {
- errors->push_back(
- make_error_str(9, "Not feasible", std::move(error_str)));
- }
- }
-
- else if (*error ==
- rmf_task::TaskPlanner::TaskPlannerError::limited_capacity)
- {
- std::string error_str =
- "[TaskPlanner] Failed to compute assignments for task_id [" + id
- + "] due to insufficient battery capacity to accommodate one or more "
- "requests by any of the robots in this fleet.";
-
- RCLCPP_ERROR(node->get_logger(), "%s", error_str.c_str());
- if (errors)
- {
- errors->push_back(
- make_error_str(9, "Not feasible", std::move(error_str)));
- }
- }
-
- else
- {
- std::string error_str =
- "[TaskPlanner] Failed to compute assignments for task_id [" + id + "]";
-
- RCLCPP_ERROR(node->get_logger(), "%s", error_str.c_str());
- if (errors)
- {
- errors->push_back(
- make_error_str(9, "Not feasible", std::move(error_str)));
- }
- }
-
- return std::nullopt;
- }
-
- const auto assignments = *assignments_ptr;
-
- if (assignments.empty())
- {
- RCLCPP_ERROR(
- node->get_logger(),
- "[TaskPlanner] Failed to compute assignments for task_id [%s]",
- id.c_str());
-
- return std::nullopt;
- }
-
- return assignments;
-}
-
//==============================================================================
const std::string& FleetUpdateHandle::fleet_name() const
{
@@ -1355,23 +1551,20 @@ void FleetUpdateHandle::add_robot(
rmf_task::State state;
state.load_basic(start[0], charger_wp.value(), 1.0);
- auto context = std::make_shared(
- RobotContext
- {
- std::move(command),
- std::move(start),
- std::move(participant),
- fleet->_pimpl->mirror,
- fleet->_pimpl->planner,
- fleet->_pimpl->activation.task,
- fleet->_pimpl->task_parameters,
- fleet->_pimpl->node,
- fleet->_pimpl->worker,
- fleet->_pimpl->default_maximum_delay,
- state,
- fleet->_pimpl->task_planner
- }
- );
+ auto context = RobotContext::make(
+ std::move(command),
+ std::move(start),
+ std::move(participant),
+ fleet->_pimpl->mirror,
+ fleet->_pimpl->planner,
+ fleet->_pimpl->emergency_planner,
+ fleet->_pimpl->activation.task,
+ fleet->_pimpl->task_parameters,
+ fleet->_pimpl->node,
+ fleet->_pimpl->worker,
+ fleet->_pimpl->default_maximum_delay,
+ state,
+ fleet->_pimpl->task_planner);
// We schedule the following operations on the worker to make sure we do not
// have a multiple read/write race condition on the FleetUpdateHandle.
@@ -1389,6 +1582,10 @@ void FleetUpdateHandle::add_robot(
if (!node)
return;
+ if (fleet->_pimpl->emergency_active)
+ {
+ context->_set_emergency(true);
+ }
// TODO(MXG): We need to perform this test because we do not currently
// support the distributed negotiation in unit test environments. We
@@ -1408,21 +1605,11 @@ void FleetUpdateHandle::add_robot(
{
if (const auto c = w.lock())
{
+ const auto& graph = c->navigation_graph();
std::stringstream ss;
ss << "Failed negotiation for [" << c->requester_id()
- << "] with these starts:";
- for (const auto& l : c->location())
- {
- ss << "\n -- t:" << l.time().time_since_epoch().count()
- << " | wp:" << l.waypoint() << " | ori:"
- << l.orientation();
- if (l.location().has_value())
- {
- const auto& p = *l.location();
- ss << " | pos:(" << p.x() << ", " << p.y() << ")";
- }
- }
- ss << "\n -- Fin --";
+ << "] with these starts:"
+ << print_starts(c->location(), graph);
std::cout << ss.str() << std::endl;
auto& last_time = *last_interrupt_time;
@@ -1434,11 +1621,14 @@ void FleetUpdateHandle::add_robot(
}
last_time = now;
- RCLCPP_INFO(
- c->node()->get_logger(),
- "Requesting replan for [%s] because it failed to negotiate",
- c->requester_id().c_str());
- c->request_replan();
+ if (!c->is_stubborn())
+ {
+ RCLCPP_INFO(
+ c->node()->get_logger(),
+ "Requesting replan for [%s] because it failed to negotiate",
+ c->requester_id().c_str());
+ c->request_replan();
+ }
}
});
context->_set_negotiation_license(std::move(negotiation_license));
@@ -1456,11 +1646,39 @@ void FleetUpdateHandle::add_robot(
if (fleet->_pimpl->broadcast_client)
broadcast_client = fleet->_pimpl->broadcast_client;
- fleet->_pimpl->task_managers.insert({context,
- TaskManager::make(
- context,
- broadcast_client,
- std::weak_ptr(fleet))});
+ const auto mgr = TaskManager::make(
+ context,
+ broadcast_client,
+ std::weak_ptr(fleet));
+
+ fleet->_pimpl->task_managers.insert({context, mgr});
+
+ const auto c_it = fleet->_pimpl
+ ->unregistered_charging_assignments.find(context->name());
+ if (c_it != fleet->_pimpl->unregistered_charging_assignments.end())
+ {
+ const auto& charging = c_it->second;
+ const auto& graph = context->navigation_graph();
+ const auto* wp = graph.find_waypoint(charging.waypoint_name);
+ if (!wp)
+ {
+ RCLCPP_ERROR(
+ fleet->_pimpl->node->get_logger(),
+ "Cannot find a waypoing named [%s] for robot [%s], which was "
+ "requested as its charging point",
+ charging.waypoint_name.c_str(),
+ context->requester_id().c_str());
+ }
+ else
+ {
+ context->_set_charging(
+ wp->index(),
+ charging.mode == charging.MODE_WAIT);
+ }
+ fleet->_pimpl->unregistered_charging_assignments.erase(c_it);
+ }
+
+ mgr->set_idle_task(fleet->_pimpl->idle_task);
// -- Calling the handle_cb should always happen last --
if (handle_cb)
@@ -1578,16 +1796,12 @@ void FleetUpdateHandle::close_lanes(std::vector lane_indices)
if (!self)
return;
- const auto& current_lane_closures =
- (*self->_pimpl->planner)->get_configuration().lane_closures();
-
bool any_changes = false;
for (const auto& lane : lane_indices)
{
- if (current_lane_closures.is_open(lane))
+ if (self->_pimpl->closed_lanes.insert(lane).second)
{
any_changes = true;
- break;
}
}
@@ -1602,14 +1816,17 @@ void FleetUpdateHandle::close_lanes(std::vector lane_indices)
for (const auto& lane : lane_indices)
{
new_lane_closures.close(lane);
- // Bookkeeping
- self->_pimpl->closed_lanes.insert(lane);
}
*self->_pimpl->planner =
std::make_shared(
new_config, rmf_traffic::agv::Planner::Options(nullptr));
+ if (self->_pimpl->emergency_active)
+ {
+ self->_pimpl->update_emergency_planner();
+ }
+
self->_pimpl->task_parameters->planner(*self->_pimpl->planner);
self->_pimpl->publish_lane_states();
@@ -1631,16 +1848,17 @@ void FleetUpdateHandle::open_lanes(std::vector lane_indices)
if (!self)
return;
- const auto& current_lane_closures =
- (*self->_pimpl->planner)->get_configuration().lane_closures();
-
+ // Note that this approach to tracking whether to open a lane will make
+ // it impossible for an external user to open a lane which has been closed
+ // by the emergency_level_for_lift behavior. For now this is intentional,
+ // but in future implementations we may want to allow users to decide if
+ // that is desirable behavior.
bool any_changes = false;
for (const auto& lane : lane_indices)
{
- if (current_lane_closures.is_closed(lane))
+ if (self->_pimpl->closed_lanes.erase(lane) > 0)
{
any_changes = true;
- break;
}
}
@@ -1655,19 +1873,31 @@ void FleetUpdateHandle::open_lanes(std::vector lane_indices)
for (const auto& lane : lane_indices)
{
new_lane_closures.open(lane);
- // Bookkeeping
- self->_pimpl->closed_lanes.erase(lane);
}
*self->_pimpl->planner =
std::make_shared(
new_config, rmf_traffic::agv::Planner::Options(nullptr));
+ if (self->_pimpl->emergency_active)
+ {
+ self->_pimpl->update_emergency_planner();
+ }
+
self->_pimpl->task_parameters->planner(*self->_pimpl->planner);
self->_pimpl->publish_lane_states();
});
}
+//==============================================================================
+void FleetUpdateHandle::set_lift_emergency_level(
+ std::string lift_name,
+ std::string emergency_level_name)
+{
+ _pimpl->emergency_level_for_lift[std::move(lift_name)] =
+ std::move(emergency_level_name);
+}
+
//==============================================================================
class FleetUpdateHandle::SpeedLimitRequest::Implementation
{
@@ -2075,7 +2305,7 @@ bool FleetUpdateHandle::set_task_planner_params(
double recharge_threshold,
double recharge_soc,
bool account_for_battery_drain,
- rmf_task::ConstRequestFactoryPtr finishing_request)
+ rmf_task::ConstRequestFactoryPtr idle_task)
{
if (battery_system &&
motion_sink &&
@@ -2101,15 +2331,19 @@ bool FleetUpdateHandle::set_task_planner_params(
const rmf_task::TaskPlanner::Options options{
false,
nullptr,
- finishing_request};
+ // The finishing request is no longer handled by the planner, we handle
+ // it separately as a waiting behavior now.
+ nullptr};
_pimpl->worker.schedule(
- [w = weak_from_this(), task_config, options](const auto&)
+ [w = weak_from_this(), task_config, options, idle_task](const auto&)
{
const auto self = w.lock();
if (!self)
return;
+ self->_pimpl->idle_task = idle_task;
+
// Here we update the task planner in all the RobotContexts.
// The TaskManagers rely on the parameters in the task planner for
// automatic retreat. Hence, we also update them whenever the
@@ -2118,7 +2352,10 @@ bool FleetUpdateHandle::set_task_planner_params(
self->_pimpl->name, std::move(task_config), std::move(options));
for (const auto& t : self->_pimpl->task_managers)
+ {
t.first->task_planner(self->_pimpl->task_planner);
+ t.second->set_idle_task(idle_task);
+ }
});
return true;
diff --git a/rmf_fleet_adapter/src/rmf_fleet_adapter/agv/Node.cpp b/rmf_fleet_adapter/src/rmf_fleet_adapter/agv/Node.cpp
index 14f5d2ddc..322281326 100644
--- a/rmf_fleet_adapter/src/rmf_fleet_adapter/agv/Node.cpp
+++ b/rmf_fleet_adapter/src/rmf_fleet_adapter/agv/Node.cpp
@@ -36,6 +36,8 @@ std::shared_ptr Node::make(
auto default_qos = rclcpp::SystemDefaultsQoS();
default_qos.keep_last(100);
+ auto transient_qos = rclcpp::SystemDefaultsQoS()
+ .reliable().keep_last(100).transient_local();
node->_door_state_obs =
node->create_observable(
@@ -55,7 +57,7 @@ std::shared_ptr Node::make(
node->_lift_request_pub =
node->create_publisher(
- AdapterLiftRequestTopicName, default_qos);
+ AdapterLiftRequestTopicName, transient_qos);
node->_task_summary_pub =
node->create_publisher(
@@ -106,6 +108,18 @@ std::shared_ptr Node::make(
node->create_publisher(
TaskApiResponses, transient_local_qos);
+ node->_mutex_group_request_pub =
+ node->create_publisher(
+ MutexGroupRequestTopicName, transient_local_qos);
+
+ node->_mutex_group_request_obs =
+ node->create_observable(
+ MutexGroupRequestTopicName, transient_local_qos);
+
+ node->_mutex_group_states_obs =
+ node->create_observable(
+ MutexGroupStatesTopicName, transient_local_qos);
+
return node;
}
@@ -229,5 +243,23 @@ auto Node::task_api_response() const -> const ApiResponsePub&
return _task_api_response_pub;
}
+//==============================================================================
+auto Node::mutex_group_request() const -> const MutexGroupRequestPub&
+{
+ return _mutex_group_request_pub;
+}
+
+//==============================================================================
+auto Node::mutex_group_request_obs() const -> const MutexGroupRequestObs&
+{
+ return _mutex_group_request_obs->observe();
+}
+
+//==============================================================================
+auto Node::mutex_group_states() const -> const MutexGroupStatesObs&
+{
+ return _mutex_group_states_obs->observe();
+}
+
} // namespace agv
} // namespace rmf_fleet_adapter
diff --git a/rmf_fleet_adapter/src/rmf_fleet_adapter/agv/Node.hpp b/rmf_fleet_adapter/src/rmf_fleet_adapter/agv/Node.hpp
index 0ced9dad7..5331b43e2 100644
--- a/rmf_fleet_adapter/src/rmf_fleet_adapter/agv/Node.hpp
+++ b/rmf_fleet_adapter/src/rmf_fleet_adapter/agv/Node.hpp
@@ -35,6 +35,8 @@
#include
#include
+#include
+#include
#include
#include
@@ -122,6 +124,17 @@ class Node : public rmf_rxcpp::Transport
using ApiResponsePub = rclcpp::Publisher::SharedPtr;
const ApiResponsePub& task_api_response() const;
+ using MutexGroupRequest = rmf_fleet_msgs::msg::MutexGroupRequest;
+ using MutexGroupRequestPub = rclcpp::Publisher::SharedPtr;
+ const MutexGroupRequestPub& mutex_group_request() const;
+
+ using MutexGroupRequestObs = rxcpp::observable;
+ const MutexGroupRequestObs& mutex_group_request_obs() const;
+
+ using MutexGroupStates = rmf_fleet_msgs::msg::MutexGroupStates;
+ using MutexGroupStatesObs = rxcpp::observable;
+ const MutexGroupStatesObs& mutex_group_states() const;
+
template
rclcpp::TimerBase::SharedPtr try_create_wall_timer(
std::chrono::duration period,
@@ -179,6 +192,9 @@ class Node : public rmf_rxcpp::Transport
FleetStatePub _fleet_state_pub;
Bridge _task_api_request_obs;
ApiResponsePub _task_api_response_pub;
+ MutexGroupRequestPub _mutex_group_request_pub;
+ Bridge _mutex_group_request_obs;
+ Bridge _mutex_group_states_obs;
};
} // namespace agv
diff --git a/rmf_fleet_adapter/src/rmf_fleet_adapter/agv/RobotContext.cpp b/rmf_fleet_adapter/src/rmf_fleet_adapter/agv/RobotContext.cpp
index efc92a5d2..0abfd7165 100644
--- a/rmf_fleet_adapter/src/rmf_fleet_adapter/agv/RobotContext.cpp
+++ b/rmf_fleet_adapter/src/rmf_fleet_adapter/agv/RobotContext.cpp
@@ -22,10 +22,325 @@
#include
#include
+#include
+
+#include
namespace rmf_fleet_adapter {
namespace agv {
+//==============================================================================
+void NavParams::search_for_location(
+ const std::string& map,
+ Eigen::Vector3d position,
+ RobotContext& context)
+{
+ auto planner = context.planner();
+ if (!planner)
+ {
+ RCLCPP_ERROR(
+ context.node()->get_logger(),
+ "Planner unavailable for robot [%s], cannot update its location",
+ context.requester_id().c_str());
+ return;
+ }
+ const auto& graph = planner->get_configuration().graph();
+ const auto now = context.now();
+ auto starts = compute_plan_starts(graph, map, position, now);
+ if (!starts.empty())
+ {
+ if (context.debug_positions)
+ {
+ std::stringstream ss;
+ ss << __FILE__ << "|" << __LINE__ << ": " << starts.size()
+ << " starts:" << print_starts(starts, graph);
+ std::cout << ss.str() << std::endl;
+ }
+ context.set_location(std::move(starts));
+ }
+ else
+ {
+ if (context.debug_positions)
+ {
+ std::cout << __FILE__ << "|" << __LINE__ << ": setting robot to LOST | "
+ << map << " <" << position.block<2, 1>(0, 0).transpose()
+ << "> orientation " << position[2] * 180.0 / M_PI << std::endl;
+ }
+ context.set_lost(Location { now, map, position });
+ }
+}
+
+//==============================================================================
+void NavParams::find_stacked_vertices(const rmf_traffic::agv::Graph& graph)
+{
+ for (std::size_t i = 0; i < graph.num_waypoints() - 1; ++i)
+ {
+ const auto& wp_i = graph.get_waypoint(i);
+ const Eigen::Vector2d p_i = wp_i.get_location();
+ const std::string& map_i = wp_i.get_map_name();
+ for (std::size_t j = i+1; j < graph.num_waypoints(); ++j)
+ {
+ const auto& wp_j = graph.get_waypoint(j);
+ const Eigen::Vector2d p_j = wp_j.get_location();
+ const std::string& map_j = wp_j.get_map_name();
+ if (map_i != map_j)
+ {
+ continue;
+ }
+
+ const double dist = (p_i - p_j).norm();
+ if (dist > max_merge_waypoint_distance)
+ {
+ continue;
+ }
+
+ // stack these waypoints
+ auto stack_i = stacked_vertices[i];
+ auto stack_j = stacked_vertices[j];
+ if (!stack_i && !stack_j)
+ {
+ // create a new stack
+ stack_i = std::make_shared>();
+ stack_j = stack_i;
+ }
+ else if (stack_i && stack_j)
+ {
+ if (stack_i != stack_j)
+ {
+ for (const std::size_t other : *stack_j)
+ {
+ stack_i->insert(other);
+ stacked_vertices[other] = stack_i;
+ }
+
+ }
+ }
+ else if (!stack_i)
+ {
+ stack_i = stack_j;
+ }
+
+ assert(stack_i);
+ stack_i->insert(i);
+ stack_i->insert(j);
+ stacked_vertices[i] = stack_i;
+ stacked_vertices[j] = stack_j;
+ }
+ }
+}
+
+//==============================================================================
+std::string NavParams::get_vertex_name(
+ const rmf_traffic::agv::Graph& graph,
+ const std::optional v_opt) const
+{
+ if (!v_opt.has_value())
+ return "";
+
+ const std::size_t v = v_opt.value();
+ if (const std::string* n = graph.get_waypoint(v).name())
+ {
+ return *n;
+ }
+ else
+ {
+ const auto& stack_it = stacked_vertices.find(v);
+ if (stack_it != stacked_vertices.end() && stack_it->second)
+ {
+ for (const auto& v : *stack_it->second)
+ {
+ if (const auto* n = graph.get_waypoint(v).name())
+ {
+ return *n;
+ }
+ }
+ }
+ }
+
+ return "";
+}
+
+//==============================================================================
+rmf_traffic::agv::Plan::StartSet NavParams::process_locations(
+ const rmf_traffic::agv::Graph& graph,
+ rmf_traffic::agv::Plan::StartSet locations) const
+{
+ return _lift_boundary_filter(graph, _descend_stacks(graph, locations));
+}
+
+//==============================================================================
+rmf_traffic::agv::Plan::StartSet NavParams::_descend_stacks(
+ const rmf_traffic::agv::Graph& graph,
+ rmf_traffic::agv::Plan::StartSet locations) const
+{
+ std::vector remove;
+ for (std::size_t i = 0; i < locations.size(); ++i)
+ {
+ rmf_traffic::agv::Plan::Start& location = locations[i];
+ std::optional waypoint_opt;
+ if (location.lane().has_value())
+ {
+ const rmf_traffic::agv::Graph::Lane& lane =
+ graph.get_lane(*location.lane());
+ waypoint_opt = lane.entry().waypoint_index();
+ }
+ else
+ {
+ waypoint_opt = location.waypoint();
+ }
+
+ if (!waypoint_opt.has_value())
+ continue;
+
+ const std::size_t original_waypoint = waypoint_opt.value();
+ std::size_t waypoint = original_waypoint;
+ const auto s_it = stacked_vertices.find(waypoint);
+ VertexStack stack;
+ if (s_it != stacked_vertices.end())
+ stack = s_it->second;
+ if (!stack)
+ continue;
+
+ std::unordered_set visited;
+ bool can_descend = true;
+ bool has_loop = false;
+ while (can_descend)
+ {
+ can_descend = false;
+ if (!visited.insert(waypoint).second)
+ {
+ // These stacked vertices have a loop so there's no way to find a bottom
+ // for it. We need to just exit here.
+ has_loop = true;
+ break;
+ }
+
+ for (std::size_t v : *stack)
+ {
+ if (graph.lane_from(v, waypoint))
+ {
+ waypoint = v;
+ can_descend = true;
+ break;
+ }
+ }
+ }
+
+ if (has_loop)
+ {
+ continue;
+ }
+
+ // Transfer the location estimate over to the waypoint that's at the bottom
+ // of the vertex stack.
+ if (waypoint != original_waypoint)
+ {
+ bool can_merge = true;
+ if (const auto r_merge = graph.get_waypoint(waypoint).merge_radius())
+ {
+ if (const auto p_opt = location.location())
+ {
+ const auto p = p_opt.value();
+ const auto p_wp = graph.get_waypoint(waypoint).get_location();
+ if ((p - p_wp).norm() > r_merge)
+ {
+ can_merge = false;
+ remove.push_back(i);
+ }
+ }
+ }
+
+ if (can_merge)
+ {
+ location.lane(std::nullopt);
+ location.waypoint(waypoint);
+ }
+ }
+ }
+
+ for (auto r_it = remove.rbegin(); r_it != remove.rend(); ++r_it)
+ {
+ locations.erase(locations.begin() + *r_it);
+ }
+
+ return locations;
+}
+
+//==============================================================================
+rmf_traffic::agv::Plan::StartSet NavParams::_lift_boundary_filter(
+ const rmf_traffic::agv::Graph& graph,
+ rmf_traffic::agv::Plan::StartSet locations) const
+{
+ const auto r_it = std::remove_if(
+ locations.begin(),
+ locations.end(),
+ [&graph](const rmf_traffic::agv::Plan::Start& location)
+ {
+ if (location.lane().has_value())
+ {
+ // If the intention is to move along a lane, then it is okay to keep
+ // this. If the lane is entering or exiting the lift, then it should
+ // have the necessary events.
+ // TODO(@mxgrey): Should we check and make sure that the event is
+ // actually present?
+ return false;
+ }
+
+ if (!location.location().has_value())
+ {
+ // If the robot is perfectly on some waypoint then there is no need to
+ // filter.
+ return false;
+ }
+
+ const Eigen::Vector2d p = location.location().value();
+
+ const auto robot_inside_lift = [&]()
+ -> rmf_traffic::agv::Graph::LiftPropertiesPtr
+ {
+ for (const auto& lift : graph.all_known_lifts())
+ {
+ // We assume lifts never overlap so we will return the first
+ // positive hit.
+ if (lift->is_in_lift(p))
+ return lift;
+ }
+
+ return nullptr;
+ }();
+
+ const auto& wp = graph.get_waypoint(location.waypoint());
+ const auto lift = wp.in_lift();
+ // If the robot's lift status and the waypoint's lift status don't match
+ // then we should filter this waypoint out.
+ return wp.in_lift() != robot_inside_lift;
+ });
+
+ locations.erase(r_it, locations.end());
+ return locations;
+}
+
+//==============================================================================
+bool NavParams::in_same_stack(
+ std::size_t wp0,
+ std::size_t wp1) const
+{
+ if (wp0 == wp1)
+ {
+ return true;
+ }
+
+ const auto s_it = stacked_vertices.find(wp0);
+ if (s_it == stacked_vertices.end())
+ return false;
+
+ const auto stack = s_it->second;
+ if (!stack)
+ return false;
+
+ return stack->count(wp1);
+}
+
//==============================================================================
std::shared_ptr RobotContext::command()
{
@@ -41,25 +356,43 @@ std::shared_ptr RobotContext::make_updater()
//==============================================================================
Eigen::Vector3d RobotContext::position() const
{
- assert(!_location.empty());
- const auto& l = _location.front();
- if (l.location().has_value())
+ if (!_location.empty())
{
- const Eigen::Vector2d& p = *l.location();
+ const auto& l = _location.front();
+ if (l.location().has_value())
+ {
+ const Eigen::Vector2d& p = *l.location();
+ return {p[0], p[1], l.orientation()};
+ }
+
+ const Eigen::Vector2d& p =
+ navigation_graph().get_waypoint(l.waypoint()).get_location();
return {p[0], p[1], l.orientation()};
}
+ else if (_lost.has_value() && _lost->location.has_value())
+ {
+ return _lost->location->position;
+ }
- const Eigen::Vector2d& p =
- navigation_graph().get_waypoint(l.waypoint()).get_location();
- return {p[0], p[1], l.orientation()};
+ throw std::runtime_error(
+ "No location information is available for [" + requester_id() + "]");
}
//==============================================================================
const std::string& RobotContext::map() const
{
- assert(!_location.empty());
- return navigation_graph()
- .get_waypoint(_location.front().waypoint()).get_map_name();
+ if (!_location.empty())
+ {
+ return navigation_graph()
+ .get_waypoint(_location.front().waypoint()).get_map_name();
+ }
+ else if (_lost.has_value() && _lost->location.has_value())
+ {
+ return _lost->location->map;
+ }
+
+ throw std::runtime_error(
+ "No location information is available for [" + requester_id() + "]");
}
//==============================================================================
@@ -83,10 +416,21 @@ const rmf_traffic::agv::Plan::StartSet& RobotContext::location() const
//==============================================================================
void RobotContext::set_location(rmf_traffic::agv::Plan::StartSet location_)
{
+ for (auto& location : location_)
+ {
+ location.orientation(rmf_utils::wrap_to_pi(location.orientation()));
+ }
+
_location = std::move(location_);
filter_closed_lanes();
+
if (_location.empty())
{
+ if (debug_positions)
+ {
+ std::cout << __FILE__ << "|" << __LINE__ << ": setting robot to LOST" <<
+ std::endl;
+ }
set_lost(std::nullopt);
return;
}
@@ -142,30 +486,17 @@ void RobotContext::set_lost(std::optional location)
//==============================================================================
void RobotContext::filter_closed_lanes()
{
- if (const auto planner = *_planner)
+ const rmf_traffic::agv::LaneClosure* closures = get_lane_closures();
+ if (closures)
{
- const auto& closures = planner->get_configuration().lane_closures();
for (std::size_t i = 0; i < _location.size(); )
{
if (_location[i].lane().has_value())
{
- if (closures.is_closed(*_location[i].lane()))
+ if (closures->is_closed(*_location[i].lane()))
{
- if (_location.size() > 1)
- {
- _location.erase(_location.begin() + i);
- continue;
- }
- else
- {
- RCLCPP_WARN(
- node()->get_logger(),
- "Robot [%s] is being forced to use closed lane [%lu] because it "
- "has not been provided any other feasible lanes to use.",
- requester_id().c_str(),
- *_location[i].lane());
- return;
- }
+ _location.erase(_location.begin() + i);
+ continue;
}
}
++i;
@@ -173,6 +504,27 @@ void RobotContext::filter_closed_lanes()
}
}
+//==============================================================================
+const rmf_traffic::agv::LaneClosure* RobotContext::get_lane_closures() const
+{
+ if (_emergency)
+ {
+ if (const auto planner = *_emergency_planner)
+ {
+ return &planner->get_configuration().lane_closures();
+ }
+ }
+ else
+ {
+ if (const auto planner = *_planner)
+ {
+ return &planner->get_configuration().lane_closures();
+ }
+ }
+
+ return nullptr;
+}
+
//==============================================================================
rmf_traffic::schedule::Participant& RobotContext::itinerary()
{
@@ -222,6 +574,12 @@ const std::string& RobotContext::requester_id() const
return _requester_id;
}
+//==============================================================================
+rmf_traffic::ParticipantId RobotContext::participant_id() const
+{
+ return _itinerary.id();
+}
+
//==============================================================================
const rmf_traffic::agv::Graph& RobotContext::navigation_graph() const
{
@@ -235,6 +593,13 @@ RobotContext::planner() const
return *_planner;
}
+//==============================================================================
+const std::shared_ptr&
+RobotContext::emergency_planner() const
+{
+ return *_emergency_planner;
+}
+
//==============================================================================
std::shared_ptr RobotContext::nav_params() const
{
@@ -306,6 +671,13 @@ RobotContext::observe_replan_request() const
return _replan_obs;
}
+//==============================================================================
+const rxcpp::observable&
+RobotContext::observe_charging_change() const
+{
+ return _charging_change_obs;
+}
+
//==============================================================================
void RobotContext::request_replan()
{
@@ -400,7 +772,7 @@ std::function RobotContext::make_get_state()
rmf_task::State state;
state.load_basic(
self->_most_recent_valid_location.front(),
- self->_charger_wp,
+ self->_charging_wp,
self->_current_battery_soc);
state.insert(GetContext{self->shared_from_this()});
@@ -420,10 +792,22 @@ const std::string* RobotContext::current_task_id() const
//==============================================================================
RobotContext& RobotContext::current_task_id(std::optional id)
{
+ std::unique_lock lock(*_current_task_id_mutex);
_current_task_id = std::move(id);
+ _initial_time_idle_outside_lift = std::nullopt;
return *this;
}
+//==============================================================================
+std::string RobotContext::copy_current_task_id() const
+{
+ std::unique_lock lock(*_current_task_id_mutex);
+ if (_current_task_id.has_value())
+ return _current_task_id.value();
+
+ return {};
+}
+
//==============================================================================
double RobotContext::current_battery_soc() const
{
@@ -450,9 +834,15 @@ RobotContext& RobotContext::current_battery_soc(const double battery_soc)
}
//==============================================================================
-std::size_t RobotContext::dedicated_charger_wp() const
+std::size_t RobotContext::dedicated_charging_wp() const
{
- return _charger_wp;
+ return _charging_wp;
+}
+
+//==============================================================================
+bool RobotContext::waiting_for_charger() const
+{
+ return _waiting_for_charger;
}
//==============================================================================
@@ -600,13 +990,118 @@ const Reporting& RobotContext::reporting() const
return _reporting;
}
+//==============================================================================
+bool RobotContext::localize(
+ EasyFullControl::Destination estimate,
+ EasyFullControl::CommandExecution execution) const
+{
+ if (_localize)
+ {
+ _localize(std::move(estimate), std::move(execution));
+ return true;
+ }
+
+ return false;
+}
+
+//==============================================================================
+void RobotContext::set_localization(
+ EasyFullControl::LocalizationRequest localization)
+{
+ _localize = std::move(localization);
+}
+
+//==============================================================================
+const LiftDestination* RobotContext::current_lift_destination() const
+{
+ return _lift_destination.get();
+}
+
+//==============================================================================
+std::shared_ptr RobotContext::set_lift_destination(
+ std::string lift_name,
+ std::string destination_floor,
+ bool requested_from_inside)
+{
+ _lift_arrived = false;
+ _lift_destination = std::make_shared(
+ LiftDestination{
+ std::move(lift_name),
+ std::move(destination_floor),
+ requested_from_inside
+ });
+ _initial_time_idle_outside_lift = std::nullopt;
+
+ _publish_lift_destination();
+ return _lift_destination;
+}
+
+//==============================================================================
+void RobotContext::release_lift()
+{
+ if (_lift_destination)
+ {
+ RCLCPP_INFO(
+ _node->get_logger(),
+ "Releasing lift [%s] for [%s]",
+ _lift_destination->lift_name.c_str(),
+ requester_id().c_str());
+ }
+ _lift_destination = nullptr;
+ _initial_time_idle_outside_lift = std::nullopt;
+ _lift_stubbornness = nullptr;
+ _lift_arrived = false;
+}
+
+//==============================================================================
+const std::optional& RobotContext::holding_door() const
+{
+ return _holding_door;
+}
+
+//==============================================================================
+const std::unordered_map&
+RobotContext::locked_mutex_groups() const
+{
+ return _locked_mutex_groups;
+}
+
+//==============================================================================
+const rxcpp::observable& RobotContext::request_mutex_groups(
+ std::unordered_set groups,
+ rmf_traffic::Time claim_time)
+{
+ const auto t = rmf_traffic_ros2::convert(claim_time);
+ for (const auto& group : groups)
+ {
+ const auto [it, inserted] = _requesting_mutex_groups.insert({group, t});
+ if (!inserted)
+ {
+ if (t.nanosec < it->second.nanosec)
+ it->second = t;
+ }
+ }
+
+ _publish_mutex_group_requests();
+ return _mutex_group_lock_obs;
+}
+
+//==============================================================================
+void RobotContext::retain_mutex_groups(
+ const std::unordered_set& retain)
+{
+ _retain_mutex_groups(retain, _requesting_mutex_groups);
+ _retain_mutex_groups(retain, _locked_mutex_groups);
+}
+
//==============================================================================
RobotContext::RobotContext(
std::shared_ptr command_handle,
std::vector _initial_location,
rmf_traffic::schedule::Participant itinerary,
std::shared_ptr schedule,
- std::shared_ptr> planner,
+ SharedPlanner planner,
+ SharedPlanner emergency_planner,
rmf_task::ConstActivatorPtr activator,
rmf_task::ConstParametersPtr parameters,
std::shared_ptr node,
@@ -619,6 +1114,7 @@ RobotContext::RobotContext(
_itinerary(std::move(itinerary)),
_schedule(std::move(schedule)),
_planner(std::move(planner)),
+ _emergency_planner(std::move(emergency_planner)),
_task_activator(std::move(activator)),
_task_parameters(std::move(parameters)),
_stubbornness(std::make_shared(0)),
@@ -627,7 +1123,7 @@ RobotContext::RobotContext(
_maximum_delay(maximum_delay),
_requester_id(
_itinerary.description().owner() + "/" + _itinerary.description().name()),
- _charger_wp(state.dedicated_charging_waypoint().value()),
+ _charging_wp(state.dedicated_charging_waypoint().value()),
_current_task_end_state(state),
_current_task_id(std::nullopt),
_task_planner(std::move(task_planner)),
@@ -639,8 +1135,9 @@ RobotContext::RobotContext(
_replan_obs = _replan_publisher.get_observable();
_graph_change_obs = _graph_change_publisher.get_observable();
-
+ _charging_change_obs = _charging_change_publisher.get_observable();
_battery_soc_obs = _battery_soc_publisher.get_observable();
+ _mutex_group_lock_obs = _mutex_group_lock_subject.get_observable();
_current_mode = rmf_fleet_msgs::msg::RobotMode::MODE_IDLE;
_override_status = std::nullopt;
@@ -648,6 +1145,96 @@ RobotContext::RobotContext(
_action_executor = nullptr;
}
+//==============================================================================
+void RobotContext::schedule_itinerary(
+ std::shared_ptr plan_id,
+ rmf_traffic::schedule::Itinerary new_itinerary)
+{
+ bool scheduled = false;
+ std::size_t attempts = 0;
+ while (!scheduled)
+ {
+ if (++attempts > 5)
+ {
+ std::stringstream ss_sizes;
+ for (const auto& r : new_itinerary)
+ {
+ ss_sizes << "[" << r.map() << ":" << r.trajectory().size() << "]";
+ }
+
+ RCLCPP_ERROR(
+ node()->get_logger(),
+ "Repeatedly failled attempts to update schedule with an itinerary "
+ "containing [%lu] routes with sizes %s during LockMutexGroup "
+ "action for robot [%s]. Last attempted value was [%lu]. We will "
+ "continue without updating the traffic schedule. This could lead to "
+ "traffic management problems. Please report this bug to the "
+ "maintainers of RMF.",
+ new_itinerary.size(),
+ ss_sizes.str().c_str(),
+ requester_id().c_str(),
+ *plan_id);
+ break;
+ }
+
+ scheduled = itinerary().set(*plan_id, new_itinerary);
+
+ if (!scheduled)
+ {
+ for (const auto& r : new_itinerary)
+ {
+ if (r.trajectory().size() < 2)
+ {
+ std::stringstream ss_sizes;
+ for (const auto& r : new_itinerary)
+ {
+ ss_sizes << "[" << r.map() << ":" << r.trajectory().size() << "]";
+ }
+
+ RCLCPP_ERROR(
+ node()->get_logger(),
+ "Attempting to schedule an itinerary for robot [%s] containing a "
+ "route that has fewer than 2 waypoints. Routes sizes are %s. "
+ "We will continue without updating the traffic schedule. This "
+ "could lead to traffic management problems. Please report this "
+ "bug to the maintainers of RMF.",
+ requester_id().c_str(),
+ ss_sizes.str().c_str());
+ return;
+ }
+ }
+
+ *plan_id = itinerary().assign_plan_id();
+ if (attempts > 1)
+ {
+ RCLCPP_ERROR(
+ node()->get_logger(),
+ "Invalid plan_id [%lu] when current plan_id is [%lu] for robot [%s] "
+ "while performing a LockMutexGroup. Please report this bug to an RMF "
+ "developer.",
+ *plan_id,
+ itinerary().current_plan_id(),
+ requester_id().c_str());
+ }
+ }
+ }
+}
+
+//==============================================================================
+void RobotContext::schedule_hold(
+ std::shared_ptr plan_id,
+ rmf_traffic::Time time,
+ rmf_traffic::Duration wait,
+ Eigen::Vector3d position,
+ const std::string& map)
+{
+ rmf_traffic::Trajectory hold;
+ const auto zero = Eigen::Vector3d::Zero();
+ hold.insert(time, position, zero);
+ hold.insert(time + wait, position, zero);
+ schedule_itinerary(plan_id, {rmf_traffic::Route(map, std::move(hold))});
+}
+
//==============================================================================
void RobotContext::_set_task_manager(std::shared_ptr