Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions client/cpp/ProjectAirsimClientLib/src/Client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -766,7 +766,12 @@ void Client::Impl::RequestSendingThreadProc(void) {
SendRequest(str_method, json_params, pasyncresultprovider_message);

// If the send failed, complete the async result with the error
if (status != Status::OK) pasyncresultprovider_message->SetDone(status);
if (status != Status::OK)
pasyncresultprovider_message->SetDone(status);
} else {
// Complete the async result so a caller blocked in Wait() is
// released.
pasyncresultprovider_message->SetDone(Status::Canceled);
}

// Done with request in this method
Expand Down Expand Up @@ -833,9 +838,11 @@ void Client::Impl::ResponseReceivingThreadProc(void) {
NNG_FLAG_ALLOC);
if (err == nngi::NNG_ETIMEDOUT) {
// Check to see whether we should exit
if (run_request_threads_)
continue; // Nope, keep going
else {
if (run_request_threads_) {
// Reply lost or never arrived within NNG_OPT_RECVTIMEO;
// fail the request instead of retrying forever.
pasyncresultprovider_message->SetDone(Status::TimedOut);
} else {
// Yes, cancel the request and exit
pasyncresultprovider_message->SetDone(Status::Canceled);
}
Expand Down
34 changes: 34 additions & 0 deletions core_sim/include/core_sim/message/clock_message.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (C) 2026 IAMAI CONSULTING CORP
//
// MIT License. All rights reserved.

#ifndef CORE_SIM_INCLUDE_CORE_SIM_MESSAGE_CLOCK_MESSAGE_HPP_
#define CORE_SIM_INCLUDE_CORE_SIM_MESSAGE_CLOCK_MESSAGE_HPP_

#include <memory>

#include "core_sim/clock.hpp"
#include "core_sim/message/message.hpp"

namespace microsoft {
namespace projectairsim {

class ClockMessage : public Message {
public:
explicit ClockMessage(TimeNano time_stamp_val);
ClockMessage();
~ClockMessage() override;

TimeNano GetTimeStamp() const;

std::string Serialize() const override;
void Deserialize(const std::string& buffer) override;

private:
class Impl;
};

} // namespace projectairsim
} // namespace microsoft

#endif // CORE_SIM_INCLUDE_CORE_SIM_MESSAGE_CLOCK_MESSAGE_HPP_
1 change: 1 addition & 0 deletions core_sim/include/core_sim/message/message.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ enum class MessageType {
kPose = 28,
kIntList = 29,
kFloat = 30,
kClock = 31,
};

class Message {
Expand Down
1 change: 1 addition & 0 deletions core_sim/include/core_sim/topic.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class Topic {
friend class EnvActor;
friend class Battery;
friend class ViewportCameraImpl;
friend class Scene;

Topic(const std::string& name, const std::string& path, TopicType type,
int frequency, MessageType message_type);
Expand Down
1 change: 1 addition & 0 deletions core_sim/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ add_library(
message/airspeed_message.cpp
message/barometer_message.cpp
message/camera_info_message.cpp
message/clock_message.cpp
message/collision_info_message.cpp
message/distance_sensor_message.cpp
message/flight_control_setpoint_message.cpp
Expand Down
62 changes: 62 additions & 0 deletions core_sim/src/message/clock_message.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (C) 2026 IAMAI CONSULTING CORP
//
// MIT License. All rights reserved.

#include "core_sim/message/clock_message.hpp"

#include <memory>
#include <sstream>

#include "message_impl.hpp"
#include "msgpack.hpp"

namespace microsoft {
namespace projectairsim {

class ClockMessage::Impl : public MessageImpl {
public:
Impl() : MessageImpl(MessageType::kClock), time_stamp(0) {}
explicit Impl(TimeNano time_stamp_val)
: MessageImpl(MessageType::kClock), time_stamp(time_stamp_val) {}

TimeNano GetTimeStamp() const { return time_stamp; }

std::string Serialize() override {
std::stringstream stream;
msgpack::packer<std::stringstream> packer(stream);
this->msgpack_pack(packer);
return stream.str();
}

void Deserialize(const std::string& buffer) override {
auto handle = msgpack::unpack(buffer.data(), buffer.size());
this->msgpack_unpack(handle.get());
}

MSGPACK_DEFINE_MAP(time_stamp);

private:
TimeNano time_stamp;
};

ClockMessage::ClockMessage() : Message(std::make_shared<ClockMessage::Impl>()) {}

ClockMessage::ClockMessage(TimeNano time_stamp_val)
: Message(std::make_shared<ClockMessage::Impl>(time_stamp_val)) {}

ClockMessage::~ClockMessage() {}

TimeNano ClockMessage::GetTimeStamp() const {
return static_cast<ClockMessage::Impl*>(pimpl_.get())->GetTimeStamp();
}

std::string ClockMessage::Serialize() const {
return static_cast<ClockMessage::Impl*>(pimpl_.get())->Serialize();
}

void ClockMessage::Deserialize(const std::string& buffer) {
static_cast<ClockMessage::Impl*>(pimpl_.get())->Deserialize(buffer);
}

} // namespace projectairsim
} // namespace microsoft
6 changes: 6 additions & 0 deletions core_sim/src/message/message_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "message_utils.hpp"

#include "core_sim/error.hpp"
#include "core_sim/message/clock_message.hpp"
#include "core_sim/message/flight_control_rc_input_message.hpp"
#include "core_sim/message/flight_control_setpoint_message.hpp"
#include "core_sim/message/int8_message.hpp"
Expand All @@ -20,6 +21,11 @@ namespace projectairsim {

Message MessageUtils::ToMessage(const Topic& topic, const std::string& buffer) {
switch (topic.GetMessageType()) {
case MessageType::kClock: {
ClockMessage message;
message.Deserialize(buffer);
return message;
}
case MessageType::kInt8: {
Int8Message message;
message.Deserialize(buffer);
Expand Down
32 changes: 32 additions & 0 deletions core_sim/src/scene.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
#include "core_sim/error.hpp"
#include "core_sim/file_utils.hpp"
#include "core_sim/geodetic_converter.hpp"
#include "core_sim/message/clock_message.hpp"
#include "core_sim/service_method.hpp"
#include "core_sim/topic.hpp"
#include "core_sim/viewport_camera.hpp"
#include "json.hpp"
#include "message/common_utils.hpp"
Expand Down Expand Up @@ -194,6 +196,8 @@ class Scene::Impl : public ComponentWithTopicsAndServiceMethods {

bool SceneTick();

void CreateTopics();

void EnableViewportCamera(bool enable);

// set enable_unreal_viewport_camera_callback_
Expand All @@ -218,6 +222,8 @@ class Scene::Impl : public ComponentWithTopicsAndServiceMethods {
std::function<void()> physics_stop_callback_;
ScheduledExecutor executor_;
TimeNano sim_time_;
Topic clock_topic_;
bool clock_topic_registered_ = false;
ClockSettings clock_settings_;
SegmentationSettings segmentation_settings_;
HomeGeoPoint home_geo_point_;
Expand Down Expand Up @@ -635,6 +641,19 @@ void Scene::Impl::StopSceneTick() {
}
}

void Scene::Impl::CreateTopics() {
constexpr TimeNano kNanosPerSecond = 1000000000LL;
const int frequency =
clock_settings_.scene_tick_period > 0
? static_cast<int>(kNanosPerSecond /
clock_settings_.scene_tick_period)
: 0;
clock_topic_ = Topic("clock", topic_path_, TopicType::kPublished, frequency,
MessageType::kClock);
topic_manager_.RegisterTopic(clock_topic_);
clock_topic_registered_ = true;
}

void Scene::Impl::OnBeginUpdate() {
for (auto& actor : actors_) {
if (actor->GetType() == ActorType::kRobot) {
Expand Down Expand Up @@ -1010,6 +1029,12 @@ bool Scene::Impl::SceneTick() {
TimeNano sim_dt_nanos = new_sim_time - sim_time_;
sim_time_ = new_sim_time;

// Publish the authoritative simulator clock from the same tick that stamps
// sensor samples. Clients must not poll GetSimTime to drive ROS /clock: a
// blocked service request would otherwise stop time for every use_sim_time
// node even though the simulation itself is still running.
topic_manager_.PublishTopic(clock_topic_, ClockMessage(sim_time_));

if (clock_settings_.type == ClockType::kSteppable &&
state_manager_.IsDistributed() && state_manager_.IsClockSource()) {
state_manager_.SendSimTime(sim_time_);
Expand Down Expand Up @@ -1192,6 +1217,8 @@ void Scene::Loader::LoadSceneWithJSON(const json& json) {
SimClock::Get(std::make_shared<SteppableClock>());
}

impl_.CreateTopics();

// Load segmentation settings for initializing scene object segmentation IDs
LoadSegmentationSettings(json);

Expand Down Expand Up @@ -1219,6 +1246,11 @@ void Scene::Loader::LoadSceneWithJSON(const json& json) {
}

void Scene::Loader::UnloadScene() {
if (impl_.clock_topic_registered_) {
impl_.topic_manager_.UnregisterTopic(impl_.clock_topic_);
impl_.clock_topic_ = Topic();
impl_.clock_topic_registered_ = false;
}
impl_.topic_manager_.SetTopicPublishedCallbackEnabled(false);
impl_.topic_manager_.SetCallbackTopicPublished(nullptr);

Expand Down
3 changes: 3 additions & 0 deletions core_sim/src/topic_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,9 @@ void TopicManager::Impl::CreateTopicList() {
case MessageType::kDistanceSensor:
topic_info.message_type = "distance-sensor";
break;
case MessageType::kClock:
topic_info.message_type = "clock";
break;
default:
break;
}
Expand Down
1 change: 1 addition & 0 deletions core_sim/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ add_executable(
gtest_unreal_vehicle_actuator.cpp
gtest_noise_model_utils.cpp
gtest_clock.cpp
gtest_clock_message.cpp
gtest_message.cpp
gtest_image_message.cpp
gtest_topic_manager.cpp
Expand Down
19 changes: 19 additions & 0 deletions core_sim/test/gtest_clock_message.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (C) 2026 IAMAI CONSULTING CORP
//
// MIT License. All rights reserved.

#include "core_sim/message/clock_message.hpp"
#include "gtest/gtest.h"

namespace projectairsim = microsoft::projectairsim;

TEST(ClockMessage, PreservesNanosecondTimestamp) {
constexpr TimeNano kTimestamp = 1234567890123456LL;
projectairsim::ClockMessage original(kTimestamp);

EXPECT_EQ(original.GetType(), projectairsim::MessageType::kClock);

projectairsim::ClockMessage unpacked;
unpacked.Deserialize(original.Serialize());
EXPECT_EQ(unpacked.GetTimeStamp(), kTimestamp);
}
19 changes: 17 additions & 2 deletions docs/ros/ros2.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@ when Project AirSim reports topic information.
| `publish_tf` | `true` | Broadcast TF transforms from `/actual_pose` and camera pose payloads. |
| `tf_world_frame_id` | `map` | Parent frame used for TF transforms. |
| `refresh_topics_period_sec` | `0.0` | Periodic topic discovery interval. `0.0` disables polling; use a positive value only if scenes/topics can change outside this node. |
| `publish_clock_period_sec` | `0.02` | Periodic `/clock` publish interval in seconds. `0.0` disables clock publishing. |
| `vehicle_name` | `Drone1` | Vehicle used for single-drone services/actions. |
| `service_root` | `/projectairsim` | Root namespace for command services and actions. |
| `image_qos_depth` | `5` | `KEEP_LAST` depth for image publishers. Values below `1` are rejected. |
Expand Down Expand Up @@ -180,6 +179,22 @@ unset FASTDDS_DEFAULT_PROFILES_FILE
unset FASTRTPS_DEFAULT_PROFILES_FILE
```

The simulator publishes `/Sim/<scene>/clock` from the scene tick. The bridge
subscribes to that native clock stream and republishes every sample on ROS
`/clock`; it does not poll `GetSimTime`. Sensor message headers likewise retain
their native per-sample `time_stamp` values. If a sensor payload has no valid
`time_stamp`, the bridge warns and publishes the conventional
invalid/uninitialized ROS timestamp (`sec: 0`, `nanosec: 0`) instead of
substituting the bridge's current time. ROS time fields are integers, so they
cannot represent NaN.

For optimized native image messages, an absent `time_stamp` or an unsigned
value larger than `INT64_MAX` produces the same zero timestamp while preserving
the image. A present `time_stamp` encoded with the wrong MessagePack type
(such as a string, floating-point value, Boolean, or null), or encoded as a
negative integer, makes the image payload malformed; the bridge warns and
drops that image instead of substituting ROS node time.

## Topics

List ROS2 topics:
Expand Down Expand Up @@ -214,7 +229,7 @@ The bridge also publishes:
| Topic | Type | Description |
|---|---|---|
| `/projectairsim/topic_info` | `std_msgs/msg/String` | JSON list of Project AirSim topic paths from the first discovery pass after startup or scene load. |
| `/clock` | `rosgraph_msgs/msg/Clock` | Project AirSim simulation time from `World::GetSimTime()`. |
| `/clock` | `rosgraph_msgs/msg/Clock` | Native Project AirSim scene-clock topic, republished without service polling. |
| `/tf` | `tf2_msgs/msg/TFMessage` | Vehicle and camera transforms when `publish_tf=true`. |

To echo a topic:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,6 @@ inline double NumberOr(const json& object, const char* key,
}

inline bool JsonToInt64(const json& value, std::int64_t* out) {
if (value.is_number_integer()) {
*out = value.get<std::int64_t>();
return true;
}
if (value.is_number_unsigned()) {
const auto unsigned_value = value.get<std::uint64_t>();
if (unsigned_value >
Expand All @@ -207,8 +203,20 @@ inline bool JsonToInt64(const json& value, std::int64_t* out) {
*out = static_cast<std::int64_t>(unsigned_value);
return true;
}
if (value.is_number_integer()) {
*out = value.get<std::int64_t>();
return true;
}
if (value.is_number_float()) {
*out = static_cast<std::int64_t>(std::llround(value.get<double>()));
const auto floating_value = value.get<double>();
if (!std::isfinite(floating_value) ||
static_cast<long double>(floating_value) <
static_cast<long double>(std::numeric_limits<std::int64_t>::min()) ||
static_cast<long double>(floating_value) >
static_cast<long double>(std::numeric_limits<std::int64_t>::max())) {
return false;
}
*out = static_cast<std::int64_t>(std::llround(floating_value));
return true;
}
return false;
Expand All @@ -229,7 +237,8 @@ inline bool ExtractSimTimeNanos(const json& value, std::int64_t* nanosec) {
}
}

for (const auto* key : {"time_nanos", "sim_time_nanos", "nanosec", "nanos"}) {
for (const auto* key : {"time_stamp", "time_nanos", "sim_time_nanos",
"nanosec", "nanos"}) {
auto it = value.find(key);
if (it != value.end() && JsonToInt64(*it, nanosec)) return true;
}
Expand Down Expand Up @@ -440,6 +449,8 @@ inline bool PopulateImagePayloadFromJson(const json& msg,
}

struct NativeImageMetadata {
std::uint64_t time_stamp = 0;
bool has_time_stamp = false;
std::string source_encoding;
float pos_x = 0.0F;
float pos_y = 0.0F;
Expand Down Expand Up @@ -539,7 +550,10 @@ inline bool PopulateImagePayloadFromMsgpack(const std::string& payload,
if (key.type != msgpack::type::STR) continue;
const std::string_view field(key.via.str.ptr, key.via.str.size);

if (field == "height") {
if (field == "time_stamp") {
parsed_metadata.time_stamp = detail::ImageUnsigned(value, "time_stamp");
parsed_metadata.has_time_stamp = true;
} else if (field == "height") {
image->height = detail::ImageUint32(value, "height");
has_height = true;
} else if (field == "width") {
Expand Down
Loading