ROS2 API Reference
This document describes every ROS 2 interface of the GR-Navigation system, including topics, services, and actions.
Environment Preparation
In every new terminal, initialize the ROS 2 and robot DDS discovery environment before running any ros2 command, Python example, or C++ example from this document:
source /opt/fftai/fourierdds/shells/discovery_env.sh --ros2
export ROS_DOMAIN_ID=123
The script loads the ROS 2 Humble environment, configures robot DDS discovery, and selects the Fast DDS middleware used by ROS 2. ROS_DOMAIN_ID must be set explicitly to match the robot's current configuration. Once the script succeeds, the standard message types used in this document and the deployed Fourier ROS 2 interface packages (such as fourier_msgs and map_manager) are all available from the ROS 2 environment.
If ros2 interface show fourier_msgs/srv/GetCurrentAction or another Fourier interface referenced here reports a missing package, the ROS 2 interface packages are not yet deployed on the robot. Complete the deployment for the matching robot software version before running these examples.
GRW robots run under a device namespace by default. Store the actual namespace in an environment variable first — on the robot you can read it from ros2 topic list, for example /GRW01AA0001/...:
export ROBOT_NAMESPACE=/GRW01AA0001 # replace with the actual robot namespace
All Python and C++ examples in this document use relative interface names. Set the namespace on the process at launch to reach that robot's interfaces directly:
python3 example.py --ros-args -r __ns:=$ROBOT_NAMESPACE
./example --ros-args -r __ns:=$ROBOT_NAMESPACE
CLI examples, by contrast, must spell out the prefix — for example $ROBOT_NAMESPACE/slam/set_mode. If the robot has no namespace, set ROBOT_NAMESPACE to an empty string and drop the extra / from the commands.
To compile or run local ROS 2 programs without connecting to a robot, run:
source /opt/ros/humble/setup.bash
export ROS_DOMAIN_ID=123
Important Notes
When the navigation system runs under a custom namespace, every node, topic, service, and action gains the namespace prefix, but the message structures stay the same.
e.g.
| Without namespace | With namespace (robot1) |
|---|---|
| /slam/set_mode | /robot1/slam/set_mode |
ros2 service call $ROBOT_NAMESPACE/slam/set_mode fourier_msgs/srv/SetMode "{mode: 'mapping'}" | ros2 service call /robot1/slam/set_mode fourier_msgs/srv/SetMode "{mode: 'mapping'}" |
| /slam/mode_status | /robot1/slam/mode_status |
| ros2 topic echo $ROBOT_NAMESPACE/slam/mode_status | ros2 topic echo /robot1/slam/mode_status |
1. Switching Between Mapping and Localization
1.1 /slam/set_mode (Service)
Switch between mapping mode and localization mode
Service Type: fourier_msgs/srv/SetMode
Example Call:
# Switch to mapping mode
ros2 service call $ROBOT_NAMESPACE/slam/set_mode fourier_msgs/srv/SetMode "{mode: 'mapping'}"
# Switch to localization mode
ros2 service call $ROBOT_NAMESPACE/slam/set_mode fourier_msgs/srv/SetMode "{mode: 'localization'}"
Python client:
from fourier_msgs.srv import SetMode
import rclpy
from rclpy.node import Node
class ModeClient(Node):
def __init__(self):
super().__init__('mode_client')
self.client = self.create_client(SetMode, 'slam/set_mode')
self.client.wait_for_service()
def switch_mode(self, mode: str):
request = SetMode.Request()
request.mode = mode
future = self.client.call_async(request)
rclpy.spin_until_future_complete(self, future)
return future.result()
# Example
rclpy.init()
client = ModeClient()
result = client.switch_mode('localization')
print(f"Success: {result.success}, Message: {result.message}")
C++ client:
#include <rclcpp/rclcpp.hpp>
#include <fourier_msgs/srv/set_mode.hpp>
class ModeClient : public rclcpp::Node {
public:
ModeClient() : Node("mode_client") {
client_ = create_client<fourier_msgs::srv::SetMode>("slam/set_mode");
client_->wait_for_service();
}
bool switch_mode(const std::string& mode) {
auto request = std::make_shared<fourier_msgs::srv::SetMode::Request>();
request->mode = mode;
auto future = client_->async_send_request(request);
if (rclcpp::spin_until_future_complete(shared_from_this(), future) ==
rclcpp::FutureReturnCode::SUCCESS) {
auto result = future.get();
RCLCPP_INFO(get_logger(), "Result: %s", result->message.c_str());
return result->success;
}
return false;
}
private:
rclcpp::Client<fourier_msgs::srv::SetMode>::SharedPtr client_;
};
// Example
int main(int argc, char** argv) {
rclcpp::init(argc, argv);
auto client = std::make_shared<ModeClient>();
client->switch_mode("localization");
rclcpp::shutdown();
return 0;
}
2. Mapping Mode APIs
2.1 /clear_map (Topic)
Clear the current map and restart mapping
Message Type: fourier_msgs_pod/msg/ShortString
Behavior:
- The message content is not parsed; on receiving any valid
ShortString, the navigation module clears the current map. - This interface clears only the current runtime map data; it does not delete saved composite map archives. To delete a saved map, use /clear_composite_map.
Publishing Example:
ros2 topic pub $ROBOT_NAMESPACE/clear_map fourier_msgs_pod/msg/ShortString "{}" --once
Python publisher:
from fourier_msgs_pod.msg import ShortString
import rclpy
from rclpy.node import Node
from rclpy.qos import DurabilityPolicy, HistoryPolicy, QoSProfile, ReliabilityPolicy
class ClearMapPublisher(Node):
def __init__(self):
super().__init__('clear_map_publisher')
qos = QoSProfile(
history=HistoryPolicy.KEEP_LAST,
depth=10,
reliability=ReliabilityPolicy.BEST_EFFORT,
durability=DurabilityPolicy.TRANSIENT_LOCAL,
)
self.publisher = self.create_publisher(ShortString, 'clear_map', qos)
def clear(self):
msg = ShortString()
self.publisher.publish(msg)
self.get_logger().info('Clear map command sent')
rclpy.init()
node = ClearMapPublisher()
try:
# Wait for DDS endpoint discovery; exiting immediately can send the one-shot command before the subscriber has matched.
rclpy.spin_once(node, timeout_sec=1.0)
node.clear()
rclpy.spin_once(node, timeout_sec=1.0)
finally:
node.destroy_node()
rclpy.shutdown()
C++ publisher:
#include <chrono>
#include <memory>
#include <rclcpp/rclcpp.hpp>
#include <thread>
#include <fourier_msgs_pod/msg/short_string.hpp>
class ClearMapPublisher : public rclcpp::Node {
public:
ClearMapPublisher() : Node("clear_map_publisher") {
const auto qos = rclcpp::QoS(rclcpp::KeepLast(10))
.best_effort()
.transient_local();
publisher_ = create_publisher<fourier_msgs_pod::msg::ShortString>("clear_map", qos);
}
void clear() {
auto msg = fourier_msgs_pod::msg::ShortString();
publisher_->publish(msg);
RCLCPP_INFO(get_logger(), "Clear map command sent");
}
private:
rclcpp::Publisher<fourier_msgs_pod::msg::ShortString>::SharedPtr publisher_;
};
int main(int argc, char** argv) {
rclcpp::init(argc, argv);
auto node = std::make_shared<ClearMapPublisher>();
rclcpp::spin_some(node);
std::this_thread::sleep_for(std::chrono::seconds(1));
node->clear();
std::this_thread::sleep_for(std::chrono::seconds(1));
rclcpp::shutdown();
return 0;
}
2.2 /map (Topic)
Publish the 2D occupancy grid map
Message Type: nav_msgs/msg/OccupancyGrid
Data Source:
- Mapping mode: the navigation module publishes the live 2D occupancy grid.
- Localization mode: the navigation module publishes the 2D occupancy grid of the currently loaded map.
Data Conventions:
| Parameter | Value | Description |
|---|---|---|
| Frame ID | map | Map coordinate frame |
| data | -1 / 0 / 100 | Unknown, free, and occupied respectively |
| Resolution, size, and origin | Runtime map data | Determined by the current map; do not hard-code them in the client |
Subscription Example:
ros2 topic echo $ROBOT_NAMESPACE/map
3. Localization Mode APIs
3.1 /initialpose (Topic)
Set the robot's initial pose
Message Type: geometry_msgs/msg/PoseWithCovarianceStamped
QoS Requirements: the subscriber on this topic uses BEST_EFFORT + TRANSIENT_LOCAL. Publishers must use the same reliability and durability policies; the Reliable + Volatile defaults of ros2 topic pub will not match the subscriber, and the message will not be processed. A one-shot --once publication may also be lost depending on endpoint matching timing, so publish continuously for a few seconds.
Publishing Example:
# Set the initial pose: position (1.0, 2.0, 0.0), yaw = 90 degrees as a quaternion
# Publish for about 6 seconds and then exit, so DDS endpoints match and the message is processed
timeout 6 ros2 topic pub --qos-reliability best_effort --qos-durability transient_local -r 1 \
$ROBOT_NAMESPACE/initialpose geometry_msgs/msg/PoseWithCovarianceStamped \
"{header: {frame_id: 'map'}, pose: {pose: {position: {x: 1.0, y: 2.0, z: 0.0}, orientation: {x: 0.0, y: 0.0, z: 0.707, w: 0.707}}}}"
Python publisher:
from geometry_msgs.msg import PoseWithCovarianceStamped
import rclpy
from rclpy.node import Node
import math
from rclpy.qos import DurabilityPolicy, HistoryPolicy, QoSProfile, ReliabilityPolicy
class InitialPosePublisher(Node):
def __init__(self):
super().__init__('initialpose_publisher')
qos = QoSProfile(
history=HistoryPolicy.KEEP_LAST,
depth=10,
reliability=ReliabilityPolicy.BEST_EFFORT,
durability=DurabilityPolicy.TRANSIENT_LOCAL,
)
self.publisher = self.create_publisher(PoseWithCovarianceStamped, 'initialpose', qos)
def set_pose(self, x: float, y: float, yaw: float):
msg = PoseWithCovarianceStamped()
msg.header.frame_id = 'map'
msg.header.stamp = self.get_clock().now().to_msg()
msg.pose.pose.position.x = x
msg.pose.pose.position.y = y
msg.pose.pose.position.z = 0.0
# Convert yaw to a quaternion
msg.pose.pose.orientation.x = 0.0
msg.pose.pose.orientation.y = 0.0
msg.pose.pose.orientation.z = math.sin(yaw / 2.0)
msg.pose.pose.orientation.w = math.cos(yaw / 2.0)
self.publisher.publish(msg)
rclpy.init()
node = InitialPosePublisher()
try:
rclpy.spin_once(node, timeout_sec=1.0)
node.set_pose(1.0, 2.0, 1.57) # x=1, y=2, yaw=90 degrees
rclpy.spin_once(node, timeout_sec=1.0)
finally:
node.destroy_node()
rclpy.shutdown()
C++ publisher:
#include <rclcpp/rclcpp.hpp>
#include <geometry_msgs/msg/pose_with_covariance_stamped.hpp>
#include <cmath>
#include <chrono>
#include <memory>
#include <thread>
class InitialPosePublisher : public rclcpp::Node {
public:
InitialPosePublisher() : Node("initialpose_publisher") {
const auto qos = rclcpp::QoS(rclcpp::KeepLast(10))
.best_effort()
.transient_local();
publisher_ = create_publisher<geometry_msgs::msg::PoseWithCovarianceStamped>("initialpose", qos);
}
void set_pose(double x, double y, double yaw) {
auto msg = geometry_msgs::msg::PoseWithCovarianceStamped();
msg.header.frame_id = "map";
msg.header.stamp = now();
msg.pose.pose.position.x = x;
msg.pose.pose.position.y = y;
msg.pose.pose.position.z = 0.0;
// Convert yaw to a quaternion
msg.pose.pose.orientation.x = 0.0;
msg.pose.pose.orientation.y = 0.0;
msg.pose.pose.orientation.z = std::sin(yaw / 2.0);
msg.pose.pose.orientation.w = std::cos(yaw / 2.0);
publisher_->publish(msg);
}
private:
rclcpp::Publisher<geometry_msgs::msg::PoseWithCovarianceStamped>::SharedPtr publisher_;
};
int main(int argc, char** argv) {
rclcpp::init(argc, argv);
auto node = std::make_shared<InitialPosePublisher>();
rclcpp::spin_some(node);
std::this_thread::sleep_for(std::chrono::seconds(1));
node->set_pose(1.0, 2.0, 1.57);
std::this_thread::sleep_for(std::chrono::seconds(1));
rclcpp::shutdown();
return 0;
}
3.2 /slam/load_map (Service)
Load a map and switch to localization mode
Service Type: fourier_msgs/srv/LoadMap
Behavior:
map_pathmust be a map archive directory saved by/save_composite_map.- The service loads the archive, switches to localization mode, and sets the initial pose from the
x,y, andyawfields of the request; thezfield is not processed on GRW.
Example Call:
ros2 service call $ROBOT_NAMESPACE/slam/load_map fourier_msgs/srv/LoadMap \
"{map_path: '/home/user/maps/office', x: 0.0, y: 0.0, z: 0.0, yaw: 0.0}"
Python client:
from fourier_msgs.srv import LoadMap
import rclpy
from rclpy.node import Node
class MapLoader(Node):
def __init__(self):
super().__init__('map_loader')
self.client = self.create_client(LoadMap, 'slam/load_map')
if not self.client.wait_for_service(timeout_sec=10.0):
raise RuntimeError('slam/load_map service unavailable')
def load(self, path: str, x=0.0, y=0.0, z=0.0, yaw=0.0):
request = LoadMap.Request()
request.map_path = path
request.x = x
request.y = y
request.z = z
request.yaw = yaw
future = self.client.call_async(request)
rclpy.spin_until_future_complete(self, future, timeout_sec=30.0)
result = future.result()
if result is None:
raise RuntimeError('slam/load_map request timed out')
print(f'result={result.result}, message={result.message}')
return result.result == 0 # 0 means success
rclpy.init()
loader = MapLoader()
try:
success = loader.load('/home/user/maps/office', x=1.0, y=2.0, yaw=1.57)
print(f'load success: {success}')
finally:
loader.destroy_node()
rclpy.shutdown()
C++ client:
#include <chrono>
#include <memory>
#include <rclcpp/rclcpp.hpp>
#include <fourier_msgs/srv/load_map.hpp>
#include <stdexcept>
#include <string>
class MapLoader : public rclcpp::Node {
public:
MapLoader() : Node("map_loader") {
client_ = create_client<fourier_msgs::srv::LoadMap>("slam/load_map");
if (!client_->wait_for_service(std::chrono::seconds(10))) {
throw std::runtime_error("slam/load_map service unavailable");
}
}
bool load(const std::string& path, double x=0.0, double y=0.0,
double z=0.0, double yaw=0.0) {
auto request = std::make_shared<fourier_msgs::srv::LoadMap::Request>();
request->map_path = path;
request->x = x;
request->y = y;
request->z = z;
request->yaw = yaw;
auto future = client_->async_send_request(request);
if (rclcpp::spin_until_future_complete(shared_from_this(), future) ==
rclcpp::FutureReturnCode::SUCCESS) {
return future.get()->result == 0;
}
return false;
}
private:
rclcpp::Client<fourier_msgs::srv::LoadMap>::SharedPtr client_;
};
int main(int argc, char** argv) {
rclcpp::init(argc, argv);
try {
auto loader = std::make_shared<MapLoader>();
const bool ok = loader->load("/home/user/maps/office", 1.0, 2.0, 0.0, 1.57);
RCLCPP_INFO(loader->get_logger(), "load success: %s", ok ? "true" : "false");
} catch (const std::exception& e) {
RCLCPP_ERROR(rclcpp::get_logger("map_loader"), "%s", e.what());
}
rclcpp::shutdown();
return 0;
}
3.3 /robot_pose (Topic)
Publish the robot's 2D pose in the map frame
Message Type: geometry_msgs/msg/PoseStamped
Description: publishes the current localization result. Position uses x and y, orientation is the quaternion derived from the yaw rotation about the Z axis, and z is 0.
Publish Rate: 10Hz
Frame ID: map
QoS Requirements: the GRW publisher uses BEST_EFFORT + TRANSIENT_LOCAL. Python and C++ subscribers must use the same reliability and durability policies; the ROS 2 default Reliable QoS will not match the publisher and no messages will be received.
Subscription Example:
ros2 topic echo $ROBOT_NAMESPACE/robot_pose
Python subscriber:
from geometry_msgs.msg import PoseStamped
import rclpy
from rclpy.node import Node
from rclpy.qos import DurabilityPolicy, HistoryPolicy, QoSProfile, ReliabilityPolicy
class PoseSubscriber(Node):
def __init__(self):
super().__init__('pose_subscriber')
qos = QoSProfile(
history=HistoryPolicy.KEEP_LAST,
depth=10,
reliability=ReliabilityPolicy.BEST_EFFORT,
durability=DurabilityPolicy.TRANSIENT_LOCAL,
)
self.subscription = self.create_subscription(
PoseStamped, 'robot_pose', self.pose_callback, qos)
def pose_callback(self, msg):
pos = msg.pose.position
ori = msg.pose.orientation
print(f"Position: x={pos.x:.3f}, y={pos.y:.3f}, z={pos.z:.3f}")
print(f"Orientation: x={ori.x:.3f}, y={ori.y:.3f}, z={ori.z:.3f}, w={ori.w:.3f}")
rclpy.init()
node = PoseSubscriber()
rclpy.spin(node)
C++ subscriber:
#include <rclcpp/rclcpp.hpp>
#include <geometry_msgs/msg/pose_stamped.hpp>
class PoseSubscriber : public rclcpp::Node {
public:
PoseSubscriber() : Node("pose_subscriber") {
const auto qos = rclcpp::QoS(rclcpp::KeepLast(10))
.best_effort()
.transient_local();
subscription_ = create_subscription<geometry_msgs::msg::PoseStamped>(
"robot_pose", qos,
[this](geometry_msgs::msg::PoseStamped::SharedPtr msg) {
RCLCPP_INFO(get_logger(), "Position: [%.3f, %.3f, %.3f]",
msg->pose.position.x, msg->pose.position.y, msg->pose.position.z);
});
}
private:
rclcpp::Subscription<geometry_msgs::msg::PoseStamped>::SharedPtr subscription_;
};
3.5 /odom_status_code (Topic)
Publish the localization system status code
Message Type: std_msgs/msg/Int8
Description: published once per second, this reports whether the localization quality query succeeded, which tells you whether the chassis can supply a localization quality value.
Status Code Definitions:
| Status Code | Name | Description |
|---|---|---|
| 0 | No localization quality | Localization quality could not be obtained |
| 1 | Localization quality available | Localization quality was obtained successfully |
QoS Requirements: the GRW publisher uses BEST_EFFORT + TRANSIENT_LOCAL. Python and C++ subscribers must use the same reliability and durability policies; the ROS 2 default Reliable QoS will not match the publisher and no messages will be received.
Subscription Example:
ros2 topic echo $ROBOT_NAMESPACE/odom_status_code
Python subscriber:
from std_msgs.msg import Int8
import rclpy
from rclpy.node import Node
from rclpy.qos import DurabilityPolicy, HistoryPolicy, QoSProfile, ReliabilityPolicy
class OdomStatusSubscriber(Node):
STATUS_NAMES = {
0: "localization quality unavailable",
1: "localization quality available",
}
def __init__(self):
super().__init__('odom_status_subscriber')
qos = QoSProfile(
history=HistoryPolicy.KEEP_LAST,
depth=10,
reliability=ReliabilityPolicy.BEST_EFFORT,
durability=DurabilityPolicy.TRANSIENT_LOCAL,
)
self.subscription = self.create_subscription(
Int8, 'odom_status_code', self.status_callback, qos)
def status_callback(self, msg):
status_name = self.STATUS_NAMES.get(msg.data, "UNKNOWN")
print(f"Localization status: {status_name} (code: {msg.data})")
if msg.data == 0:
print("Localization quality not obtained")
rclpy.init()
node = OdomStatusSubscriber()
rclpy.spin(node)
C++ subscriber:
#include <rclcpp/rclcpp.hpp>
#include <std_msgs/msg/int8.hpp>
class OdomStatusSubscriber : public rclcpp::Node {
public:
OdomStatusSubscriber() : Node("odom_status_subscriber") {
const auto qos = rclcpp::QoS(rclcpp::KeepLast(10))
.best_effort()
.transient_local();
subscription_ = create_subscription<std_msgs::msg::Int8>(
"odom_status_code", qos,
[this](std_msgs::msg::Int8::SharedPtr msg) {
std::string status_name = getStatusName(msg->data);
RCLCPP_INFO(get_logger(), "Localization status: %s (code: %d)",
status_name.c_str(), msg->data);
if (msg->data == 0) {
RCLCPP_WARN(get_logger(), "Localization quality not obtained");
}
});
}
private:
std::string getStatusName(int8_t code) {
switch(code) {
case 0: return "localization quality unavailable";
case 1: return "localization quality available";
default: return "UNKNOWN";
}
}
rclcpp::Subscription<std_msgs::msg::Int8>::SharedPtr subscription_;
};
3.6 /odom_status_score (Topic)
Publish the localization confidence score
Message Type: std_msgs/msg/Int8
Description: published once per second, this is the localization quality score. The range is 0–100; 0 is published when localization quality could not be obtained.
Data Notes:
data= localization quality score (0–100)- When
/odom_status_codeis 0,datais 0
QoS Requirements: the GRW publisher uses BEST_EFFORT + TRANSIENT_LOCAL. Python and C++ subscribers must use the same reliability and durability policies; the ROS 2 default Reliable QoS will not match the publisher and no messages will be received.
Subscription Example:
ros2 topic echo $ROBOT_NAMESPACE/odom_status_score
Python subscriber (combined with the status code):
from std_msgs.msg import Int8
import rclpy
from rclpy.node import Node
from rclpy.qos import DurabilityPolicy, HistoryPolicy, QoSProfile, ReliabilityPolicy
class OdomMonitor(Node):
def __init__(self):
super().__init__('odom_monitor')
self.status_code = 0
self.status_score = 0
qos = QoSProfile(
history=HistoryPolicy.KEEP_LAST,
depth=10,
reliability=ReliabilityPolicy.BEST_EFFORT,
durability=DurabilityPolicy.TRANSIENT_LOCAL,
)
self.code_sub = self.create_subscription(
Int8, 'odom_status_code', self.code_callback, qos)
self.score_sub = self.create_subscription(
Int8, 'odom_status_score', self.score_callback, qos)
def code_callback(self, msg):
self.status_code = msg.data
self.print_status()
def score_callback(self, msg):
self.status_score = msg.data
self.print_status()
def print_status(self):
if self.status_code == 1:
print(f"Localization quality score: {self.status_score}/100")
else:
print("Localization quality not obtained")
rclpy.init()
node = OdomMonitor()
rclpy.spin(node)
C++ subscriber (combined with the status code):
#include <rclcpp/rclcpp.hpp>
#include <std_msgs/msg/int8.hpp>
class OdomMonitor : public rclcpp::Node {
public:
OdomMonitor() : Node("odom_monitor"), status_code_(0), status_score_(0) {
const auto qos = rclcpp::QoS(rclcpp::KeepLast(10))
.best_effort()
.transient_local();
code_sub_ = create_subscription<std_msgs::msg::Int8>(
"odom_status_code", qos,
[this](std_msgs::msg::Int8::SharedPtr msg) {
status_code_ = msg->data;
print_status();
});
score_sub_ = create_subscription<std_msgs::msg::Int8>(
"odom_status_score", qos,
[this](std_msgs::msg::Int8::SharedPtr msg) {
status_score_ = msg->data;
print_status();
});
}
private:
void print_status() {
if (status_code_ == 1) {
RCLCPP_INFO(get_logger(), "Localization quality score: %d/100", status_score_);
} else {
RCLCPP_WARN(get_logger(), "Localization quality not obtained");
}
}
rclcpp::Subscription<std_msgs::msg::Int8>::SharedPtr code_sub_;
rclcpp::Subscription<std_msgs::msg::Int8>::SharedPtr score_sub_;
int8_t status_code_;
int8_t status_score_;
};
3.7 Relocalization
Trigger the chassis to re-estimate the robot pose when localization is lost or the pose is uncertain.
3.7.2 /slam/trigger_local_relocalization (Service)
Trigger chassis relocalization
Service Type: fourier_msgs/srv/VPRLocalRelocalization
Behavior: the current implementation does not use polygon_vertices; the call triggers relocalization over an unrestricted search area. Local relocalization bounded by a polygon is not supported. The candidate_count field in the response is always 0 and must not be read as a number of candidate poses.
Example Call:
ros2 service call $ROBOT_NAMESPACE/slam/trigger_local_relocalization \
fourier_msgs/srv/VPRLocalRelocalization \
"{polygon_vertices: []}"
4. Navigation APIs
4.1 navigate_to_pose (Action)
Navigate to a target pose
Action Type: nav2_msgs/action/NavigateToPose
Implementation Notes: GRW provides the standard ROS 2 NavigateToPose action and accepts only one goal at a time; while a goal is still running, a new goal request is rejected, so the client must cancel the current goal first. Goal coordinates should use the map frame. The server reads the goal's x and y plus the planar heading computed from the quaternion's z and w; it does not validate or transform frame_id.
Terminal Status Codes (as implemented):
- Goal terminal states are published as bytes on the status topic and in the
get_resultresponse:4=succeeded,5=canceled,6=aborted(consistent with the grstate contract). - This numbering is the opposite of the standard
action_msgsconstants (CANCELED=4,SUCCEEDED=5). Clients inside the delivered robot environment (CLI,rclpy,rclcpp) match the implementation and display the state correctly; an unmodified standard ROS 2 Humble environment reads success (4) as CANCELED. This is a known compatibility issue. Until the release that aligns the numbering, interpret terminal states by their byte semantics in external standard environments. - A goal canceled through
/cancel_current_actionstops the robot and reports terminal state5(canceled), which requiresfourier-third-navito include commit5655cae(fix(nav): report canceled goals as CANCELED, not SUCCEEDED). Earlier versions report terminal state4after a cancel, which is indistinguishable from success; there, judge only by the service response and by the robot actually stopping (see 4.4). - During a chassis hard emergency stop: the goal is still accepted but the robot does not move, and the server reports
ABORTED(6) after about 30 seconds. Resend the goal after the emergency stop is released.
Version Requirements: for a standard ROS 2 ActionClient to discover this action and receive feedback, the robot's fourier-third-navi must include commit 4c7ef59 (fix(nav): align action feedback QoS with ROS 2 clients, which makes the feedback publisher RELIABLE + TRANSIENT_LOCAL) or later. On earlier versions the CLI, Python, and C++ action clients wait for the server indefinitely and cannot be used.
Goal Request
| Field | Type | Description |
|---|---|---|
| pose | geometry_msgs/PoseStamped | Target pose. GRW uses position.x, position.y, and the planar heading represented by orientation.z and orientation.w; send it in the map frame. |
| behavior_tree | string | Part of the standard action type; the current GRW implementation does not parse this field, so leave it empty. |
Result
The NavigateToPose_Result of nav2_msgs in ROS 2 Humble is an empty structure. The server fills in no result fields, and the CLI returns result: {}. Navigation success must be judged from the action's terminal status code (see "Terminal Status Codes" above); do not read any field from the result payload.
Live Feedback
| Field | Type | Description |
|---|---|---|
| current_pose | geometry_msgs/PoseStamped | Only pose.position.x and pose.position.y are currently written; the header, orientation, and remaining position fields are not filled in, so this must not be used as a complete pose message. |
The current implementation does not provide navigation duration, estimated time remaining, recovery count, or distance remaining. Subscribe to /robot_pose when you need the full robot pose.
Example Call
CLI:
# Navigate to position (1.0, 2.0) with yaw = 45 degrees
ros2 action send_goal $ROBOT_NAMESPACE/navigate_to_pose nav2_msgs/action/NavigateToPose \
"{pose: {header: {frame_id: 'map'}, pose: {position: {x: 1.0, y: 2.0, z: 0.0}, orientation: {x: 0.0, y: 0.0, z: 0.383, w: 0.924}}}}"
# With feedback
ros2 action send_goal $ROBOT_NAMESPACE/navigate_to_pose nav2_msgs/action/NavigateToPose \
"{pose: {header: {frame_id: 'map'}, pose: {position: {x: 1.0, y: 2.0, z: 0.0}, orientation: {x: 0.0, y: 0.0, z: 0.383, w: 0.924}}}}" --feedback
Python client:
from geometry_msgs.msg import PoseStamped
from nav2_msgs.action import NavigateToPose
import math
import rclpy
from rclpy.action import ActionClient
from rclpy.node import Node
class NavigateClient(Node):
def __init__(self):
super().__init__('navigate_client')
self.client = ActionClient(self, NavigateToPose, 'navigate_to_pose')
def send_goal(self, x: float, y: float, yaw: float):
goal = NavigateToPose.Goal()
goal.pose = PoseStamped()
goal.pose.header.frame_id = 'map'
goal.pose.header.stamp = self.get_clock().now().to_msg()
goal.pose.pose.position.x = x
goal.pose.pose.position.y = y
goal.pose.pose.orientation.z = math.sin(yaw / 2.0)
goal.pose.pose.orientation.w = math.cos(yaw / 2.0)
goal.behavior_tree = ''
self.client.wait_for_server()
return self.client.send_goal_async(goal)
rclpy.init()
node = NavigateClient()
node.send_goal(1.0, 2.0, 0.785)
rclpy.spin(node)
C++:
#include <rclcpp/rclcpp.hpp>
#include <rclcpp_action/rclcpp_action.hpp>
#include <nav2_msgs/action/navigate_to_pose.hpp>
#include <cmath>
using NavigateToPose = nav2_msgs::action::NavigateToPose;
using GoalHandleNavigateToPose = rclcpp_action::ClientGoalHandle<NavigateToPose>;
class NavigationClient : public rclcpp::Node {
public:
NavigationClient() : Node("navigation_client") {
client_ = rclcpp_action::create_client<NavigateToPose>(
this, "navigate_to_pose");
client_->wait_for_action_server();
}
void navigate_to(double x, double y, double yaw) {
// Build the goal
auto goal = NavigateToPose::Goal();
goal.pose.header.frame_id = "map";
goal.pose.header.stamp = now();
goal.pose.pose.position.x = x;
goal.pose.pose.position.y = y;
goal.pose.pose.position.z = 0.0;
goal.pose.pose.orientation.x = 0.0;
goal.pose.pose.orientation.y = 0.0;
goal.pose.pose.orientation.z = std::sin(yaw / 2.0);
goal.pose.pose.orientation.w = std::cos(yaw / 2.0);
// Set up the callbacks
auto send_goal_options = rclcpp_action::Client<NavigateToPose>::SendGoalOptions();
// Feedback callback: the current implementation fills in only current_pose x and y
send_goal_options.feedback_callback =
[this](GoalHandleNavigateToPose::SharedPtr,
const std::shared_ptr<const NavigateToPose::Feedback> feedback) {
RCLCPP_INFO(get_logger(), "Current position: [%.2f, %.2f]",
feedback->current_pose.pose.position.x,
feedback->current_pose.pose.position.y);
};
// Result callback
send_goal_options.result_callback =
[this](const GoalHandleNavigateToPose::WrappedResult& result) {
switch (result.code) {
case rclcpp_action::ResultCode::SUCCEEDED:
RCLCPP_INFO(get_logger(), "Navigation succeeded!");
break;
case rclcpp_action::ResultCode::ABORTED:
RCLCPP_ERROR(get_logger(), "Navigation aborted");
break;
case rclcpp_action::ResultCode::CANCELED:
RCLCPP_WARN(get_logger(), "Navigation canceled");
break;
default:
RCLCPP_ERROR(get_logger(), "Unknown result");
break;
}
};
// Send the goal
client_->async_send_goal(goal, send_goal_options);
}
void cancel_navigation() {
client_->async_cancel_all_goals();
}
private:
rclcpp_action::Client<NavigateToPose>::SharedPtr client_;
};
int main(int argc, char** argv) {
rclcpp::init(argc, argv);
auto client = std::make_shared<NavigationClient>();
client->navigate_to(1.0, 2.0, 0.785); // x=1, y=2, yaw=45 degrees
rclcpp::spin(client);
rclcpp::shutdown();
return 0;
}
4.2 /plan (Topic)
Publish the global planned path
Message Type: nav_msgs/msg/Path
Description: publishes the current planned path in the map frame.
Publish Rate: 1Hz
QoS Requirements: the GRW publisher uses BEST_EFFORT + TRANSIENT_LOCAL. Python and C++ subscribers must use the same reliability and durability policies; the ROS 2 default Reliable QoS will not match the publisher and no messages will be received.
Subscription Example:
ros2 topic echo $ROBOT_NAMESPACE/plan
4.4 cancel_current_action (Service)
Cancel the navigation action currently running
Service Type: fourier_msgs/srv/CancelCurrentAction
Behavior:
- After the call, the running navigation action stops and the robot comes to a halt; a response of
success=true, message='ok'means the service executed. - Calling it with no action in progress also returns
success=true, message='ok'; it does not fail with "no goal to cancel". - After a cancel, the
navigate_to_poseterminal state is5(canceled), which requiresfourier-third-navito include commit5655caeor later. Earlier versions report terminal state4after a cancel, which is indistinguishable from success (see the terminal status codes in 4.1); there, judge whether the cancel took effect only from the service response and from the robot actually stopping. - Send the cancel as early as possible: a freshly started process (especially a C++ client) introduces a few seconds of DDS discovery delay, and a short-distance goal may be reached before the cancel takes effect.
Example Call:
ros2 service call $ROBOT_NAMESPACE/cancel_current_action fourier_msgs/srv/CancelCurrentAction
Python client:
from fourier_msgs.srv import CancelCurrentAction
import rclpy
from rclpy.node import Node
class ActionCanceller(Node):
def __init__(self):
super().__init__('action_canceller')
self.client = self.create_client(CancelCurrentAction, 'cancel_current_action')
self.client.wait_for_service()
def cancel(self):
request = CancelCurrentAction.Request()
future = self.client.call_async(request)
rclpy.spin_until_future_complete(self, future)
result = future.result()
return result.success
rclpy.init()
canceller = ActionCanceller()
if canceller.cancel():
print("Action cancelled successfully")
C++ client:
#include <rclcpp/rclcpp.hpp>
#include <fourier_msgs/srv/cancel_current_action.hpp>
class ActionCanceller : public rclcpp::Node {
public:
ActionCanceller() : Node("action_canceller") {
client_ = create_client<fourier_msgs::srv::CancelCurrentAction>(
"cancel_current_action");
client_->wait_for_service();
}
bool cancel() {
auto request = std::make_shared<fourier_msgs::srv::CancelCurrentAction::Request>();
auto future = client_->async_send_request(request);
if (rclcpp::spin_until_future_complete(shared_from_this(), future) ==
rclcpp::FutureReturnCode::SUCCESS) {
return future.get()->success;
}
return false;
}
private:
rclcpp::Client<fourier_msgs::srv::CancelCurrentAction>::SharedPtr client_;
};
4.5 get_current_action (Service)
Get information about the action currently running
Service Type: fourier_msgs/srv/GetCurrentAction
Behavior: returns the name, description, and status code of the current action. The status code numbering matches the terminal status codes in 4.1 (4=succeeded, 5=canceled, 6=aborted; 2 while running and 0 when idle). Note that it is the opposite of the standard action_msgs constants — see the notes in 4.1. This service is suitable for displaying current action information; the navigation terminal state must come from the terminal status code of the navigate_to_pose action (the result payload is empty, see 4.1). For the status code reference, see get_current_action.
Example Call:
ros2 service call $ROBOT_NAMESPACE/get_current_action fourier_msgs/srv/GetCurrentAction
Python client:
from fourier_msgs.srv import GetCurrentAction
import rclpy
from rclpy.node import Node
class ActionMonitor(Node):
def __init__(self):
super().__init__('action_monitor')
self.client = self.create_client(GetCurrentAction, 'get_current_action')
self.client.wait_for_service()
def get_status(self):
request = GetCurrentAction.Request()
future = self.client.call_async(request)
rclpy.spin_until_future_complete(self, future)
result = future.result()
if result.success:
print(f"Action: {result.action_name}")
print(f"Status: {result.status_description}")
return result
rclpy.init()
monitor = ActionMonitor()
monitor.get_status()
C++ client:
#include <rclcpp/rclcpp.hpp>
#include <fourier_msgs/srv/get_current_action.hpp>
class ActionMonitor : public rclcpp::Node {
public:
ActionMonitor() : Node("action_monitor") {
client_ = create_client<fourier_msgs::srv::GetCurrentAction>(
"get_current_action");
client_->wait_for_service();
}
void get_status() {
auto request = std::make_shared<fourier_msgs::srv::GetCurrentAction::Request>();
auto future = client_->async_send_request(request);
if (rclcpp::spin_until_future_complete(shared_from_this(), future) ==
rclcpp::FutureReturnCode::SUCCESS) {
auto result = future.get();
if (result->success) {
RCLCPP_INFO(get_logger(), "Action: %s, Status: %s",
result->action_name.c_str(),
result->status_description.c_str());
}
}
}
private:
rclcpp::Client<fourier_msgs::srv::GetCurrentAction>::SharedPtr client_;
};
8. Event Notification APIs
The system provides event notifications that publish important events during navigation in real time.
8.1 /Humanoid_nav/events (Topic)
Publish aggregated system events
Message Type: fourier_msgs/msg/EventsInfo
Publish Rate: 1Hz
QoS Requirements: the GRW publisher uses BEST_EFFORT + TRANSIENT_LOCAL. Python and C++ subscribers must use the same reliability and durability policies; the ROS 2 default Reliable QoS will not match the publisher and no messages will be received.
Subscription Example:
ros2 topic echo $ROBOT_NAMESPACE/Humanoid_nav/events
The current implementation publishes only the navigation_active event, and only while a navigation action is running.
Python subscriber:
from fourier_msgs.msg import EventsInfo
import rclpy
from rclpy.node import Node
from rclpy.qos import DurabilityPolicy, HistoryPolicy, QoSProfile, ReliabilityPolicy
class EventsSubscriber(Node):
def __init__(self):
super().__init__('events_subscriber')
qos = QoSProfile(
history=HistoryPolicy.KEEP_LAST,
depth=10,
reliability=ReliabilityPolicy.BEST_EFFORT,
durability=DurabilityPolicy.TRANSIENT_LOCAL,
)
self.subscription = self.create_subscription(
EventsInfo, 'Humanoid_nav/events', self.events_callback, qos)
def events_callback(self, msg):
for event in msg.events:
print(f"[{event.event_type}] {event.message} (from: {event.source})")
rclpy.init()
rclpy.spin(EventsSubscriber())
C++ subscriber:
#include <rclcpp/rclcpp.hpp>
#include <fourier_msgs/msg/events_info.hpp>
class EventsSubscriber : public rclcpp::Node {
public:
EventsSubscriber() : Node("events_subscriber") {
const auto qos = rclcpp::QoS(rclcpp::KeepLast(10))
.best_effort()
.transient_local();
subscription_ = create_subscription<fourier_msgs::msg::EventsInfo>(
"Humanoid_nav/events", qos,
[this](fourier_msgs::msg::EventsInfo::SharedPtr msg) {
for (const auto& event : msg->events) {
RCLCPP_INFO(get_logger(), "[%s] %s (from: %s)",
event.event_type.c_str(), event.message.c_str(), event.source.c_str());
}
});
}
private:
rclcpp::Subscription<fourier_msgs::msg::EventsInfo>::SharedPtr subscription_;
};
9. Map Management APIs
GRW uses a single-layer map. This section lists the composite map, POI, virtual wall, and forbidden area services that the navigation module actually registers.
9.1 Message Type Definitions
9.1.1 CompositeMapInfo Message
Message Type: map_manager/msg/CompositeMapInfo
This message is used in the response of /load_composite_map. GRW currently fills in only map_id on a successful load, and its value is the archive directory name.
| Field | Type | Description |
|---|---|---|
| map_id | string | Identifier of the loaded map archive. |
9.1.2 POI Message
Message Type: map_manager/msg/POI
A POI marks a 2D point on the map and is managed through /poi/add, /poi/list, and /poi/remove.
| Field | Type | Description |
|---|---|---|
| id | string | POI identifier; used when adding, listing, and removing. |
| name | string | POI name. |
| pose | geometry_msgs/Pose2D | 2D pose using x, y, and theta. |
9.1.3 VirtualWall Message
Message Type: map_manager/msg/VirtualWall
A virtual wall is defined by two 2D endpoints and is managed through /wall/add, /wall/list, /wall/update, and /wall/remove.
| Field | Type | Description |
|---|---|---|
| id | int32 | Numeric virtual wall identifier; returned by the service after a successful add, and used when updating and removing. |
| start | geometry_msgs/Point | Start point, using x and y. |
| end | geometry_msgs/Point | End point, using x and y. |
9.1.4 ForbiddenArea Message
Message Type: map_manager/msg/ForbiddenArea
A forbidden area is defined by an axis-aligned rectangle of four vertices and is managed through /forbidden/add, /forbidden/list, /forbidden/update, and /forbidden/remove.
| Field | Type | Description |
|---|---|---|
| id | int32 | Numeric forbidden area identifier; returned by the service after a successful add, and used when updating and removing. |
| boundary | geometry_msgs/Polygon | points must be four vertices forming an axis-aligned rectangle. |
floor_id is a reserved field in the POI, virtual wall, and forbidden area messages above; GRW does not use its value.
9.2 Composite Map Services
| Service | Type | Description |
|---|---|---|
/save_composite_map | map_manager/srv/SaveCompositeMap | Requests that the current map be saved to a target directory; the map archive along with map.pgm and map.yaml is produced only when success=true |
/load_composite_map | map_manager/srv/LoadCompositeMap | Loads a map from an archive directory; set the initial pose through /initialpose after loading |
/clear_composite_map | map_manager/srv/ClearCompositeMap | Clears the current runtime map without deleting saved archives |
/delete_map_archive | map_manager/srv/SaveCompositeMap | Deletes the saved archive named by map_path without clearing the current runtime map |
Example Call:
ros2 service call $ROBOT_NAMESPACE/save_composite_map map_manager/srv/SaveCompositeMap \
"{map_path: '/data/maps/office', compress: false}"
ros2 service call $ROBOT_NAMESPACE/load_composite_map map_manager/srv/LoadCompositeMap \
"{map_path: '/data/maps/office'}"
ros2 service call $ROBOT_NAMESPACE/clear_composite_map map_manager/srv/ClearCompositeMap "{dummy: false}"
compress is not currently an export-format switch on GRW; use a directory path and rely on saved_path in the response.
Behavior Notes: in rare cases this service can return success=false, message='save failed'; retrying after a robot restart usually recovers. Keep any existing working archive as a fallback.
Annotation Lifecycle: POIs, virtual walls, and forbidden areas are annotations on the current runtime map. /save_composite_map stores the annotations together with the map archive, and they are restored when that archive is loaded. When the map is switched from the system side (console or cloud), the system rebuilds the current map from the annotations it saved itself, so annotations added through this interface do not survive a system-side map switch. Manage annotations through the system channel when they must persist across map switches.
9.3 Semantic Element Services
| Object | Add | List | Update | Remove |
|---|---|---|---|---|
| POI | /poi/add map_manager/srv/AddPOI | /poi/list map_manager/srv/ListPOIs | Not supported | /poi/remove map_manager/srv/RemovePOI |
| Virtual wall | /wall/add map_manager/srv/AddVirtualWall | /wall/list map_manager/srv/ListVirtualWalls | /wall/update map_manager/srv/AddVirtualWall | /wall/remove map_manager/srv/RemoveVirtualWall |
| Forbidden area | /forbidden/add map_manager/srv/AddForbiddenArea | /forbidden/list map_manager/srv/ListForbiddenAreas | /forbidden/update map_manager/srv/AddForbiddenArea | /forbidden/remove map_manager/srv/RemoveForbiddenArea |
Constraints:
- GRW ignores
floor_idin POIs, virtual walls, and forbidden areas. - Virtual walls and forbidden areas are identified by the numeric ID returned by the service; pass that ID in the object's
idfield when updating. - Forbidden areas support only axis-aligned rectangles of four vertices; other polygons will fail.