EasyFleet
A simple-by-default framework for multi-robot, multi-capability fleets built on ROS 2
Loading...
Searching...
No Matches
status_markers.hpp
1// Copyright 2026 Intelligent Robotics Lab
2//
3// This file is part of the project EasyFleet
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16#ifndef EASYFLEET_MISSION_MANAGER__STATUS_MARKERS_HPP_
17#define EASYFLEET_MISSION_MANAGER__STATUS_MARKERS_HPP_
18
19#include <mutex>
20#include <string>
21#include <unordered_map>
22
23#include "rclcpp/rclcpp.hpp"
24#include "visualization_msgs/msg/marker.hpp"
25#include "visualization_msgs/msg/marker_array.hpp"
26
27namespace easyfleet_mission_manager
28{
29
47{
48public:
67 rclcpp::Node & node,
68 const std::string & topic = "mission_status_markers",
69 double height = 0.75,
70 double text_size = 0.25)
71 : node_(node),
72 height_(height),
73 text_size_(text_size)
74 {
75 // Transient-local: a subscriber (RViz) that joins after the mission has
76 // already started still needs to see every robot's last known status
77 // immediately, not wait for its next transition.
78 pub_ = node_.create_publisher<visualization_msgs::msg::MarkerArray>(
79 topic, rclcpp::QoS(10).reliable().transient_local());
80 // set_status() is only called at mission phase transitions -- sometimes
81 // minutes apart (kLongTimeout in this scenario's own mission script) --
82 // but a marker's `header.stamp` must stay recent for RViz to keep
83 // resolving "<robot>/base_link" and moving the text with the robot: a
84 // stamp left over from the last set_status() call ages out of RViz's TF
85 // buffer between updates, at which point RViz can no longer transform
86 // the marker and it freezes/falls back to the fixed frame's origin.
87 // Re-publishing every robot's current marker on a short timer instead
88 // (rather than a message meant to be interpreted once) keeps every
89 // stamp within a couple hundred ms of "now" at all times, independent
90 // of how often the *text* itself actually changes.
91 refresh_timer_ = node_.create_wall_timer(
92 std::chrono::milliseconds(200), [this] {publish_all();});
93 }
94
102 void set_status(const std::string & robot, const std::string & text)
103 {
104 {
105 std::lock_guard<std::mutex> lock(mutex_);
106 texts_[robot] = text;
107 marker_id(robot);
108 }
109 publish_all();
110 }
111
112private:
113 void publish_all()
114 {
115 visualization_msgs::msg::MarkerArray array;
116 std::lock_guard<std::mutex> lock(mutex_);
117 const rclcpp::Time stamp = node_.get_clock()->now();
118 for (const auto & [robot, text] : texts_) {
119 visualization_msgs::msg::Marker marker;
120 marker.header.frame_id = robot + "/base_link";
121 marker.header.stamp = stamp;
122 marker.ns = "mission_status";
123 marker.id = marker_ids_.at(robot);
124 marker.type = visualization_msgs::msg::Marker::TEXT_VIEW_FACING;
125 marker.action = visualization_msgs::msg::Marker::ADD;
126 // Tells RViz to re-transform this marker against the *latest*
127 // available "<robot>/base_link" transform on every render frame,
128 // instead of looking up the transform at this message's exact
129 // header.stamp -- an exact-time lookup is racy under sim time (the
130 // publishing node's clock sample can be a tick ahead of the latest
131 // transform actually broadcast yet), which is what was producing
132 // RViz's "Lookup would require extrapolation into the future" error
133 // and the marker falling back to the fixed frame's origin.
134 marker.frame_locked = true;
135 marker.pose.position.z = height_;
136 marker.pose.orientation.w = 1.0;
137 marker.scale.z = text_size_;
138 marker.color.r = 1.0;
139 marker.color.g = 1.0;
140 marker.color.b = 1.0;
141 marker.color.a = 1.0;
142 marker.text = text;
143 // Slightly longer than the refresh period, so a marker never visibly
144 // blinks out between two refresh ticks.
145 marker.lifetime = rclcpp::Duration(std::chrono::milliseconds(500));
146 array.markers.push_back(marker);
147 }
148 if (!array.markers.empty()) {
149 pub_->publish(array);
150 }
151 }
152
153 // Requires mutex_ to already be held by the caller.
154 int32_t marker_id(const std::string & robot)
155 {
156 auto it = marker_ids_.find(robot);
157 if (it != marker_ids_.end()) {
158 return it->second;
159 }
160 const int32_t id = next_id_++;
161 marker_ids_.emplace(robot, id);
162 return id;
163 }
164
165 rclcpp::Node & node_;
166 rclcpp::Publisher<visualization_msgs::msg::MarkerArray>::SharedPtr pub_;
167 rclcpp::TimerBase::SharedPtr refresh_timer_;
168 double height_;
169 double text_size_;
170
171 std::mutex mutex_;
172 std::unordered_map<std::string, std::string> texts_;
173 std::unordered_map<std::string, int32_t> marker_ids_;
174 int32_t next_id_{0};
175};
176
177} // namespace easyfleet_mission_manager
178
179#endif // EASYFLEET_MISSION_MANAGER__STATUS_MARKERS_HPP_
void set_status(const std::string &robot, const std::string &text)
Sets robot's current status text and publishes it immediately (the background refresh timer then keep...
Definition status_markers.hpp:102
StatusMarkerPublisher(rclcpp::Node &node, const std::string &topic="mission_status_markers", double height=0.75, double text_size=0.25)
Constructs the marker publisher.
Definition status_markers.hpp:66