Skip to main content

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:

Bash
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/...:

Bash
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:

Bash
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:

Bash
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 namespaceWith 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_statusros2 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:

Bash
# 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:

Python
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:

C++
#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:

Bash
ros2 topic pub $ROBOT_NAMESPACE/clear_map fourier_msgs_pod/msg/ShortString "{}" --once

Python publisher:

Python
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:

C++
#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:

ParameterValueDescription
Frame IDmapMap coordinate frame
data-1 / 0 / 100Unknown, free, and occupied respectively
Resolution, size, and originRuntime map dataDetermined by the current map; do not hard-code them in the client

Subscription Example:

Bash
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:

Bash
# 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:

Python
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:

C++
#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_path must 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, and yaw fields of the request; the z field is not processed on GRW.

Example Call:

Bash
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:

Python
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:

C++
#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:

Bash
ros2 topic echo $ROBOT_NAMESPACE/robot_pose

Python subscriber:

Python
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:

C++
#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 CodeNameDescription
0No localization qualityLocalization quality could not be obtained
1Localization quality availableLocalization 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:

Bash
ros2 topic echo $ROBOT_NAMESPACE/odom_status_code

Python subscriber:

Python
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:

C++
#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_code is 0, data is 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:

Bash
ros2 topic echo $ROBOT_NAMESPACE/odom_status_score

Python subscriber (combined with the status code):

Python
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):

C++
#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:

Bash
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_result response: 4=succeeded, 5=canceled, 6=aborted (consistent with the grstate contract).
  • This numbering is the opposite of the standard action_msgs constants (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_action stops the robot and reports terminal state 5 (canceled), which requires fourier-third-navi to include commit 5655cae (fix(nav): report canceled goals as CANCELED, not SUCCEEDED). Earlier versions report terminal state 4 after 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​

FieldTypeDescription
posegeometry_msgs/PoseStampedTarget 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_treestringPart 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​

FieldTypeDescription
current_posegeometry_msgs/PoseStampedOnly 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:

Bash
# 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:

Python
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++:

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:

Bash
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_pose terminal state is 5 (canceled), which requires fourier-third-navi to include commit 5655cae or later. Earlier versions report terminal state 4 after 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:

Bash
ros2 service call $ROBOT_NAMESPACE/cancel_current_action fourier_msgs/srv/CancelCurrentAction

Python client:

Python
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:

C++
#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:

Bash
ros2 service call $ROBOT_NAMESPACE/get_current_action fourier_msgs/srv/GetCurrentAction

Python client:

Python
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:

C++
#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:

Bash
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:

Python
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:

C++
#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.

FieldTypeDescription
map_idstringIdentifier 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.

FieldTypeDescription
idstringPOI identifier; used when adding, listing, and removing.
namestringPOI name.
posegeometry_msgs/Pose2D2D 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.

FieldTypeDescription
idint32Numeric virtual wall identifier; returned by the service after a successful add, and used when updating and removing.
startgeometry_msgs/PointStart point, using x and y.
endgeometry_msgs/PointEnd 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.

FieldTypeDescription
idint32Numeric forbidden area identifier; returned by the service after a successful add, and used when updating and removing.
boundarygeometry_msgs/Polygonpoints 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​

ServiceTypeDescription
/save_composite_mapmap_manager/srv/SaveCompositeMapRequests 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_mapmap_manager/srv/LoadCompositeMapLoads a map from an archive directory; set the initial pose through /initialpose after loading
/clear_composite_mapmap_manager/srv/ClearCompositeMapClears the current runtime map without deleting saved archives
/delete_map_archivemap_manager/srv/SaveCompositeMapDeletes the saved archive named by map_path without clearing the current runtime map

Example Call:

Bash
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​

ObjectAddListUpdateRemove
POI/poi/add map_manager/srv/AddPOI/poi/list map_manager/srv/ListPOIsNot 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_id in 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 id field when updating.
  • Forbidden areas support only axis-aligned rectangles of four vertices; other polygons will fail.