Skip to content

File node.hpp

File List > agnocast_wrapper > node.hpp

Go to the documentation of this file

// Copyright 2025 TIER IV, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#pragma once

#include "nebula_core_ros/agnocast_wrapper/nebula_agnocast_wrapper.hpp"

#include <rclcpp/rclcpp.hpp>

#include <rclcpp/version.h>

#include <chrono>
#include <map>
#include <memory>
#include <stdexcept>
#include <string>
#include <type_traits>
#include <utility>
#include <variant>
#include <vector>

namespace nebula::agnocast_wrapper
{
// rclcpp 28+ (Jazzy) renamed OnParametersSetCallbackType to OnSetParametersCallbackType
// and removed the old name from NodeParametersInterface. Humble uses rclcpp 16.x with the old name.
#if RCLCPP_VERSION_MAJOR >= 28
using OnSetParametersCallbackType =
  rclcpp::node_interfaces::NodeParametersInterface::OnSetParametersCallbackType;
#else
using OnSetParametersCallbackType =
  rclcpp::node_interfaces::NodeParametersInterface::OnParametersSetCallbackType;
#endif
}  // namespace nebula::agnocast_wrapper

#ifdef USE_AGNOCAST_ENABLED

#include <agnocast/node/agnocast_node.hpp>

namespace nebula::agnocast_wrapper
{

class Node : public std::enable_shared_from_this<Node>
{
public:
  using SharedPtr = std::shared_ptr<Node>;

  explicit Node(
    const std::string & node_name, const rclcpp::NodeOptions & options = rclcpp::NodeOptions());

  explicit Node(
    const std::string & node_name, const std::string & namespace_,
    const rclcpp::NodeOptions & options = rclcpp::NodeOptions());

  virtual ~Node() = default;

  // Non-copyable, non-movable: copying would alias the same backend behind two wrappers.
  Node(const Node &) = delete;
  Node & operator=(const Node &) = delete;
  Node(Node &&) = delete;
  Node & operator=(Node &&) = delete;

  // ===== Basic information =====
  const char * get_name() const;
  const char * get_namespace() const;
  const char * get_fully_qualified_name() const;
  rclcpp::Logger get_logger() const;

  // ===== Time =====
  rclcpp::Clock::SharedPtr get_clock();
  rclcpp::Time now() const;

  // ===== Node interfaces =====
  rclcpp::node_interfaces::NodeBaseInterface::SharedPtr get_node_base_interface() const;
  rclcpp::node_interfaces::NodeTopicsInterface::SharedPtr get_node_topics_interface() const;
  rclcpp::node_interfaces::NodeParametersInterface::SharedPtr get_node_parameters_interface() const;

  // ===== Callback groups =====
  rclcpp::CallbackGroup::SharedPtr create_callback_group(
    rclcpp::CallbackGroupType group_type, bool automatically_add_to_executor_with_node = true);

  // ===== Parameters (non-template) =====
  const rclcpp::ParameterValue & declare_parameter(
    const std::string & name, const rclcpp::ParameterValue & default_value,
    const rcl_interfaces::msg::ParameterDescriptor & descriptor =
      rcl_interfaces::msg::ParameterDescriptor{},
    bool ignore_override = false);

  const rclcpp::ParameterValue & declare_parameter(
    const std::string & name, rclcpp::ParameterType type,
    const rcl_interfaces::msg::ParameterDescriptor & descriptor =
      rcl_interfaces::msg::ParameterDescriptor{},
    bool ignore_override = false);

  bool has_parameter(const std::string & name) const;
  void undeclare_parameter(const std::string & name);
  rclcpp::Parameter get_parameter(const std::string & name) const;
  bool get_parameter(const std::string & name, rclcpp::Parameter & parameter) const;
  std::vector<rclcpp::Parameter> get_parameters(const std::vector<std::string> & names) const;

  rcl_interfaces::msg::SetParametersResult set_parameter(const rclcpp::Parameter & parameter);
  std::vector<rcl_interfaces::msg::SetParametersResult> set_parameters(
    const std::vector<rclcpp::Parameter> & parameters);
  rcl_interfaces::msg::SetParametersResult set_parameters_atomically(
    const std::vector<rclcpp::Parameter> & parameters);

  rcl_interfaces::msg::ParameterDescriptor describe_parameter(const std::string & name) const;
  std::vector<rcl_interfaces::msg::ParameterDescriptor> describe_parameters(
    const std::vector<std::string> & names) const;
  std::vector<uint8_t> get_parameter_types(const std::vector<std::string> & names) const;
  rcl_interfaces::msg::ListParametersResult list_parameters(
    const std::vector<std::string> & prefixes, uint64_t depth) const;

  rclcpp::node_interfaces::OnSetParametersCallbackHandle::SharedPtr add_on_set_parameters_callback(
    OnSetParametersCallbackType callback);
  void remove_on_set_parameters_callback(
    const rclcpp::node_interfaces::OnSetParametersCallbackHandle * const handler);

  // ===== Parameters (template) =====
  template <typename ParameterT>
  auto declare_parameter(
    const std::string & name, const ParameterT & default_value,
    const rcl_interfaces::msg::ParameterDescriptor & descriptor =
      rcl_interfaces::msg::ParameterDescriptor{},
    bool ignore_override = false)
  {
    try {
      return declare_parameter(
               name, rclcpp::ParameterValue(default_value), descriptor, ignore_override)
        .get<ParameterT>();
    } catch (const rclcpp::ParameterTypeException & ex) {
      throw rclcpp::exceptions::InvalidParameterTypeException(name, ex.what());
    }
  }

  template <typename ParameterT>
  ParameterT declare_parameter(
    const std::string & name,
    const rcl_interfaces::msg::ParameterDescriptor & descriptor =
      rcl_interfaces::msg::ParameterDescriptor{},
    bool ignore_override = false)
  {
    rclcpp::ParameterValue value{ParameterT{}};
    return declare_parameter(name, value.get_type(), descriptor, ignore_override).get<ParameterT>();
  }

  template <typename ParameterT>
  bool get_parameter(const std::string & name, ParameterT & parameter) const
  {
    rclcpp::Parameter param;
    if (get_parameter(name, param)) {
      parameter = param.get_value<ParameterT>();
      return true;
    }
    return false;
  }

  template <typename ParameterT>
  bool get_parameters(const std::string & prefix, std::map<std::string, ParameterT> & values) const
  {
    std::map<std::string, rclcpp::Parameter> params;
    auto params_interface = get_node_parameters_interface();
    bool result = params_interface->get_parameters_by_prefix(prefix, params);
    if (result) {
      for (const auto & param : params) {
        values[param.first] = param.second.get_value<ParameterT>();
      }
    }
    return result;
  }

  // ===== Publisher =====
  template <typename MessageT>
  typename Publisher<MessageT>::SharedPtr create_publisher(
    const std::string & topic_name, const rclcpp::QoS & qos,
    const agnocast::PublisherOptions & options = agnocast::PublisherOptions{})
  {
    return visit_node([&](auto & n) -> typename Publisher<MessageT>::SharedPtr {
      using NodeT = std::decay_t<decltype(*n)>;
      if constexpr (std::is_same_v<NodeT, agnocast::Node>) {
        return std::make_shared<AgnocastPublisher<MessageT>>(n.get(), topic_name, qos, options);
      } else {
        return std::make_shared<ROS2Publisher<MessageT>>(n.get(), topic_name, qos, options);
      }
    });
  }

  template <typename MessageT>
  typename Publisher<MessageT>::SharedPtr create_publisher(
    const std::string & topic_name, size_t qos_history_depth)
  {
    return create_publisher<MessageT>(topic_name, rclcpp::QoS(rclcpp::KeepLast(qos_history_depth)));
  }

  // ===== Subscription =====
  template <typename MessageT, typename Func>
  typename Subscription<MessageT>::SharedPtr create_subscription(
    const std::string & topic_name, const rclcpp::QoS & qos, Func && callback,
    const agnocast::SubscriptionOptions & options = agnocast::SubscriptionOptions{})
  {
    return visit_node([&](auto & n) -> typename Subscription<MessageT>::SharedPtr {
      using NodeT = std::decay_t<decltype(*n)>;
      if constexpr (std::is_same_v<NodeT, agnocast::Node>) {
        return std::make_shared<AgnocastSubscription<MessageT>>(
          n.get(), topic_name, qos, std::forward<Func>(callback), options);
      } else {
        return std::make_shared<ROS2Subscription<MessageT>>(
          n.get(), topic_name, qos, std::forward<Func>(callback), options);
      }
    });
  }

  template <typename MessageT, typename Func>
  typename Subscription<MessageT>::SharedPtr create_subscription(
    const std::string & topic_name, size_t qos_history_depth, Func && callback,
    const agnocast::SubscriptionOptions & options = agnocast::SubscriptionOptions{})
  {
    return create_subscription<MessageT>(
      topic_name, rclcpp::QoS(rclcpp::KeepLast(qos_history_depth)), std::forward<Func>(callback),
      options);
  }

  // ===== Client / Service =====
  template <typename ServiceT>
  NEBULA_CLIENT_PTR(ServiceT)
  create_client(
    const std::string & service_name, const rclcpp::QoS & qos = rclcpp::ServicesQoS(),
    rclcpp::CallbackGroup::SharedPtr group = nullptr)
  {
    return visit_node([&](auto & n) -> NEBULA_CLIENT_PTR(ServiceT) {
      using NodeT = std::decay_t<decltype(*n)>;
      if constexpr (std::is_same_v<NodeT, agnocast::Node>) {
        return std::make_shared<AgnocastClient<ServiceT>>(n.get(), service_name, qos, group);
      } else {
        return std::make_shared<ROS2Client<ServiceT>>(n.get(), service_name, qos, group);
      }
    });
  }

  // Service with a callback taking NEBULA_SERVER_REQUEST_PTR/RESPONSE_PTR (message_ptr).
  template <
    typename ServiceT, typename Func,
    std::enable_if_t<is_message_ptr_service_callback_v<Func, ServiceT>, int> = 0>
  NEBULA_SERVICE_PTR(ServiceT)
  create_service(
    const std::string & service_name, Func && callback,
    const rclcpp::QoS & qos = rclcpp::ServicesQoS(),
    rclcpp::CallbackGroup::SharedPtr group = nullptr)
  {
    return visit_node([&](auto & n) -> NEBULA_SERVICE_PTR(ServiceT) {
      using NodeT = std::decay_t<decltype(*n)>;
      if constexpr (std::is_same_v<NodeT, agnocast::Node>) {
        return std::make_shared<AgnocastService<ServiceT>>(
          n.get(), service_name, std::forward<Func>(callback), qos, group);
      } else {
        return std::make_shared<ROS2Service<ServiceT>>(
          n.get(), service_name, std::forward<Func>(callback), qos, group);
      }
    });
  }

  // Convenience overload for rclcpp-style std::shared_ptr callbacks. Gives the callback independent
  // owning copies of the request/response (safe to retain, like rclcpp) and copies the filled
  // response back, so prefer the message_ptr overload above to avoid the copies on hot paths.
  template <
    typename ServiceT, typename Func,
    std::enable_if_t<
      !is_message_ptr_service_callback_v<Func, ServiceT> &&
        is_shared_ptr_service_callback_v<Func, ServiceT>,
      int> = 0>
  NEBULA_SERVICE_PTR(ServiceT)
  create_service(
    const std::string & service_name, Func && callback,
    const rclcpp::QoS & qos = rclcpp::ServicesQoS(),
    rclcpp::CallbackGroup::SharedPtr group = nullptr)
  {
    return create_service<ServiceT>(
      service_name,
      [callback = std::forward<Func>(callback)](
        NEBULA_SERVER_REQUEST_PTR(ServiceT) && req, NEBULA_SERVER_RESPONSE_PTR(ServiceT) && res) {
        auto request = std::make_shared<typename ServiceT::Request>(*req);
        auto response = std::make_shared<typename ServiceT::Response>();
        callback(request, response);
        *res = *response;
      },
      qos, group);
  }

  // Fallback overload: neither callback form matched. Exists only to turn the otherwise opaque
  // "no matching function" error into the static_assert message below.
  template <
    typename ServiceT, typename Func,
    std::enable_if_t<
      !is_message_ptr_service_callback_v<Func, ServiceT> &&
        !is_shared_ptr_service_callback_v<Func, ServiceT>,
      int> = 0>
  NEBULA_SERVICE_PTR(ServiceT)
  create_service(
    const std::string & /*service_name*/, Func && /*callback*/,
    const rclcpp::QoS & = rclcpp::ServicesQoS(), rclcpp::CallbackGroup::SharedPtr = nullptr)
  {
    static_assert(
      is_message_ptr_service_callback_v<Func, ServiceT> ||
        is_shared_ptr_service_callback_v<Func, ServiceT>,
      "Service callback must be invocable with "
      "(NEBULA_SERVER_REQUEST_PTR(ServiceT), NEBULA_SERVER_RESPONSE_PTR(ServiceT)) or with "
      "(std::shared_ptr<ServiceT::Request>, std::shared_ptr<ServiceT::Response>).");
  }

  // ===== Timer =====
  template <typename DurationRepT = int64_t, typename DurationT = std::milli, typename CallbackT>
  Timer::SharedPtr create_wall_timer(
    std::chrono::duration<DurationRepT, DurationT> period, CallbackT && callback,
    rclcpp::CallbackGroup::SharedPtr group = nullptr)
  {
    return visit_node([&](auto & n) -> Timer::SharedPtr {
      using NodeT = std::decay_t<decltype(*n)>;
      if constexpr (std::is_same_v<NodeT, agnocast::Node>) {
        return std::make_shared<AgnocastTimer>(
          n->create_wall_timer(period, std::forward<CallbackT>(callback), group));
      } else {
        return std::make_shared<ROS2Timer>(
          n->create_wall_timer(period, std::forward<CallbackT>(callback), group));
      }
    });
  }

  // ===== Internal node access (for Executor) =====
  // Callers must check use_agnocast() before calling get_agnocast_node()/get_rclcpp_node().
  // Accessing the inactive variant will throw std::runtime_error.
  // The return value is fixed for the lifetime of the Node (see class-level @invariant).

  std::shared_ptr<agnocast::Node> get_agnocast_node() const
  {
    if (auto * p = std::get_if<std::shared_ptr<agnocast::Node>>(&node_)) {
      return *p;
    }
    throw std::runtime_error(
      "get_agnocast_node() called but Agnocast is not enabled. "
      "Check use_agnocast() before calling this method.");
  }

  std::shared_ptr<rclcpp::Node> get_rclcpp_node() const
  {
    if (auto * p = std::get_if<std::shared_ptr<rclcpp::Node>>(&node_)) {
      return *p;
    }
    throw std::runtime_error(
      "get_rclcpp_node() called but the node is in agnocast mode. "
      "Check !use_agnocast() before calling this method.");
  }

private:
  using NodeVariant = std::variant<std::shared_ptr<rclcpp::Node>, std::shared_ptr<agnocast::Node>>;
  NodeVariant node_;

  template <typename Visitor>
  decltype(auto) visit_node(Visitor && vis)
  {
    return std::visit(std::forward<Visitor>(vis), node_);
  }

  template <typename Visitor>
  decltype(auto) visit_node(Visitor && vis) const
  {
    return std::visit(std::forward<Visitor>(vis), node_);
  }
};

template <typename T>
std::shared_ptr<rclcpp::Node> to_rclcpp_node(const std::shared_ptr<T> & node)
{
  return node->get_rclcpp_node();
}

template <typename CallbackT>
Timer::SharedPtr create_timer(
  Node * node, rclcpp::Clock::SharedPtr clock, rclcpp::Duration period, CallbackT && callback,
  rclcpp::CallbackGroup::SharedPtr group = nullptr)
{
  if (use_agnocast()) {
    return std::make_shared<AgnocastTimer>(agnocast::create_timer(
      node->get_agnocast_node().get(), clock, period, std::forward<CallbackT>(callback), group));
  }
  return std::make_shared<ROS2Timer>(rclcpp::create_timer(
    node->get_rclcpp_node().get(), clock, period, std::forward<CallbackT>(callback), group));
}

}  // namespace nebula::agnocast_wrapper

#else

namespace nebula::agnocast_wrapper
{

// is_message_ptr_service_callback_v / is_shared_ptr_service_callback_v are defined in
// nebula_agnocast_wrapper.hpp, identically named in both builds.

class Node : public std::enable_shared_from_this<Node>
{
public:
  using SharedPtr = std::shared_ptr<Node>;

  explicit Node(
    const std::string & node_name, const rclcpp::NodeOptions & options = rclcpp::NodeOptions())
  : node_(std::make_shared<rclcpp::Node>(node_name, options))
  {
  }

  explicit Node(
    const std::string & node_name, const std::string & namespace_,
    const rclcpp::NodeOptions & options = rclcpp::NodeOptions())
  : node_(std::make_shared<rclcpp::Node>(node_name, namespace_, options))
  {
  }

  virtual ~Node() = default;

  // Non-copyable, non-movable: copying would alias the same backend behind two wrappers.
  Node(const Node &) = delete;
  Node & operator=(const Node &) = delete;
  Node(Node &&) = delete;
  Node & operator=(Node &&) = delete;

  // ===== Basic information =====
  const char * get_name() const { return node_->get_name(); }
  const char * get_namespace() const { return node_->get_namespace(); }
  const char * get_fully_qualified_name() const { return node_->get_fully_qualified_name(); }
  rclcpp::Logger get_logger() const { return node_->get_logger(); }

  // ===== Time =====
  rclcpp::Clock::SharedPtr get_clock() { return node_->get_clock(); }
  rclcpp::Time now() const { return node_->now(); }

  // ===== Node interfaces =====
  rclcpp::node_interfaces::NodeBaseInterface::SharedPtr get_node_base_interface() const
  {
    return node_->get_node_base_interface();
  }
  rclcpp::node_interfaces::NodeTopicsInterface::SharedPtr get_node_topics_interface() const
  {
    return node_->get_node_topics_interface();
  }
  rclcpp::node_interfaces::NodeParametersInterface::SharedPtr get_node_parameters_interface() const
  {
    return node_->get_node_parameters_interface();
  }

  // ===== Callback groups =====
  rclcpp::CallbackGroup::SharedPtr create_callback_group(
    rclcpp::CallbackGroupType group_type, bool automatically_add_to_executor_with_node = true)
  {
    return node_->create_callback_group(group_type, automatically_add_to_executor_with_node);
  }

  // ===== Parameters (non-template) =====
  const rclcpp::ParameterValue & declare_parameter(
    const std::string & name, const rclcpp::ParameterValue & default_value,
    const rcl_interfaces::msg::ParameterDescriptor & descriptor =
      rcl_interfaces::msg::ParameterDescriptor{},
    bool ignore_override = false)
  {
    return node_->declare_parameter(name, default_value, descriptor, ignore_override);
  }

  const rclcpp::ParameterValue & declare_parameter(
    const std::string & name, rclcpp::ParameterType type,
    const rcl_interfaces::msg::ParameterDescriptor & descriptor =
      rcl_interfaces::msg::ParameterDescriptor{},
    bool ignore_override = false)
  {
    return node_->declare_parameter(name, type, descriptor, ignore_override);
  }

  bool has_parameter(const std::string & name) const { return node_->has_parameter(name); }
  void undeclare_parameter(const std::string & name) { node_->undeclare_parameter(name); }
  rclcpp::Parameter get_parameter(const std::string & name) const
  {
    return node_->get_parameter(name);
  }
  bool get_parameter(const std::string & name, rclcpp::Parameter & parameter) const
  {
    return node_->get_parameter(name, parameter);
  }
  std::vector<rclcpp::Parameter> get_parameters(const std::vector<std::string> & names) const
  {
    return node_->get_parameters(names);
  }

  rcl_interfaces::msg::SetParametersResult set_parameter(const rclcpp::Parameter & parameter)
  {
    return node_->set_parameter(parameter);
  }
  std::vector<rcl_interfaces::msg::SetParametersResult> set_parameters(
    const std::vector<rclcpp::Parameter> & parameters)
  {
    return node_->set_parameters(parameters);
  }
  rcl_interfaces::msg::SetParametersResult set_parameters_atomically(
    const std::vector<rclcpp::Parameter> & parameters)
  {
    return node_->set_parameters_atomically(parameters);
  }

  rcl_interfaces::msg::ParameterDescriptor describe_parameter(const std::string & name) const
  {
    return node_->describe_parameter(name);
  }
  std::vector<rcl_interfaces::msg::ParameterDescriptor> describe_parameters(
    const std::vector<std::string> & names) const
  {
    return node_->describe_parameters(names);
  }
  std::vector<uint8_t> get_parameter_types(const std::vector<std::string> & names) const
  {
    return node_->get_parameter_types(names);
  }
  rcl_interfaces::msg::ListParametersResult list_parameters(
    const std::vector<std::string> & prefixes, uint64_t depth) const
  {
    return node_->list_parameters(prefixes, depth);
  }

  rclcpp::node_interfaces::OnSetParametersCallbackHandle::SharedPtr add_on_set_parameters_callback(
    OnSetParametersCallbackType callback)
  {
    return node_->add_on_set_parameters_callback(std::move(callback));
  }
  void remove_on_set_parameters_callback(
    const rclcpp::node_interfaces::OnSetParametersCallbackHandle * const handler)
  {
    node_->remove_on_set_parameters_callback(handler);
  }

  // ===== Parameters (template) =====
  template <typename ParameterT>
  auto declare_parameter(
    const std::string & name, const ParameterT & default_value,
    const rcl_interfaces::msg::ParameterDescriptor & descriptor =
      rcl_interfaces::msg::ParameterDescriptor{},
    bool ignore_override = false)
  {
    try {
      return declare_parameter(
               name, rclcpp::ParameterValue(default_value), descriptor, ignore_override)
        .get<ParameterT>();
    } catch (const rclcpp::ParameterTypeException & ex) {
      throw rclcpp::exceptions::InvalidParameterTypeException(name, ex.what());
    }
  }

  template <typename ParameterT>
  ParameterT declare_parameter(
    const std::string & name,
    const rcl_interfaces::msg::ParameterDescriptor & descriptor =
      rcl_interfaces::msg::ParameterDescriptor{},
    bool ignore_override = false)
  {
    rclcpp::ParameterValue value{ParameterT{}};
    return declare_parameter(name, value.get_type(), descriptor, ignore_override).get<ParameterT>();
  }

  template <typename ParameterT>
  bool get_parameter(const std::string & name, ParameterT & parameter) const
  {
    rclcpp::Parameter param;
    if (get_parameter(name, param)) {
      parameter = param.get_value<ParameterT>();
      return true;
    }
    return false;
  }

  template <typename ParameterT>
  bool get_parameters(const std::string & prefix, std::map<std::string, ParameterT> & values) const
  {
    std::map<std::string, rclcpp::Parameter> params;
    auto params_interface = get_node_parameters_interface();
    bool result = params_interface->get_parameters_by_prefix(prefix, params);
    if (result) {
      for (const auto & param : params) {
        values[param.first] = param.second.get_value<ParameterT>();
      }
    }
    return result;
  }

  // ===== Publisher =====
  template <typename MessageT>
  typename rclcpp::Publisher<MessageT>::SharedPtr create_publisher(
    const std::string & topic_name, const rclcpp::QoS & qos,
    const rclcpp::PublisherOptions & options = rclcpp::PublisherOptions{})
  {
    return node_->create_publisher<MessageT>(topic_name, qos, options);
  }

  template <typename MessageT>
  typename rclcpp::Publisher<MessageT>::SharedPtr create_publisher(
    const std::string & topic_name, size_t qos_history_depth)
  {
    return node_->create_publisher<MessageT>(
      topic_name, rclcpp::QoS(rclcpp::KeepLast(qos_history_depth)));
  }

  // ===== Subscription =====
  template <typename MessageT, typename Func>
  typename rclcpp::Subscription<MessageT>::SharedPtr create_subscription(
    const std::string & topic_name, const rclcpp::QoS & qos, Func && callback,
    const rclcpp::SubscriptionOptions & options = rclcpp::SubscriptionOptions{})
  {
    return node_->create_subscription<MessageT>(
      topic_name, qos, std::forward<Func>(callback), options);
  }

  template <typename MessageT, typename Func>
  typename rclcpp::Subscription<MessageT>::SharedPtr create_subscription(
    const std::string & topic_name, size_t qos_history_depth, Func && callback,
    const rclcpp::SubscriptionOptions & options = rclcpp::SubscriptionOptions{})
  {
    return node_->create_subscription<MessageT>(
      topic_name, rclcpp::QoS(rclcpp::KeepLast(qos_history_depth)), std::forward<Func>(callback),
      options);
  }

  // ===== Client =====
  template <typename ServiceT>
  NEBULA_CLIENT_PTR(ServiceT)
  create_client(
    const std::string & service_name, const rclcpp::QoS & qos = rclcpp::ServicesQoS(),
    rclcpp::CallbackGroup::SharedPtr group = nullptr)
  {
    return nebula::agnocast_wrapper::create_client<ServiceT>(node_.get(), service_name, qos, group);
  }

  // ===== Service =====
  // Service with a callback taking NEBULA_SERVER_REQUEST_PTR/RESPONSE_PTR.
  template <
    typename ServiceT, typename Func,
    std::enable_if_t<is_message_ptr_service_callback_v<Func, ServiceT>, int> = 0>
  NEBULA_SERVICE_PTR(ServiceT)
  create_service(
    const std::string & service_name, Func && callback,
    const rclcpp::QoS & qos = rclcpp::ServicesQoS(),
    rclcpp::CallbackGroup::SharedPtr group = nullptr)
  {
    return nebula::agnocast_wrapper::create_service<ServiceT>(
      node_.get(), service_name, std::forward<Func>(callback), qos, group);
  }

  // Convenience overload for rclcpp-style std::shared_ptr callbacks: adapts to the message_ptr-
  // style callback above rather than forwarding directly, since ROS2Service only accepts that
  // shape.
  template <
    typename ServiceT, typename Func,
    std::enable_if_t<
      !is_message_ptr_service_callback_v<Func, ServiceT> &&
        is_shared_ptr_service_callback_v<Func, ServiceT>,
      int> = 0>
  NEBULA_SERVICE_PTR(ServiceT)
  create_service(
    const std::string & service_name, Func && callback,
    const rclcpp::QoS & qos = rclcpp::ServicesQoS(),
    rclcpp::CallbackGroup::SharedPtr group = nullptr)
  {
    return create_service<ServiceT>(
      service_name,
      [callback = std::forward<Func>(callback)](
        NEBULA_SERVER_REQUEST_PTR(ServiceT) && req, NEBULA_SERVER_RESPONSE_PTR(ServiceT) && res) {
        auto request = std::make_shared<typename ServiceT::Request>(*req);
        auto response = std::make_shared<typename ServiceT::Response>();
        callback(request, response);
        *res = *response;
      },
      qos, group);
  }

  // Fallback overload: neither callback form matched. Exists only to turn the otherwise opaque
  // "no matching function" error into the static_assert message below (mirrors the Agnocast build).
  template <
    typename ServiceT, typename Func,
    std::enable_if_t<
      !is_message_ptr_service_callback_v<Func, ServiceT> &&
        !is_shared_ptr_service_callback_v<Func, ServiceT>,
      int> = 0>
  NEBULA_SERVICE_PTR(ServiceT)
  create_service(
    const std::string & /*service_name*/, Func && /*callback*/,
    const rclcpp::QoS & = rclcpp::ServicesQoS(), rclcpp::CallbackGroup::SharedPtr = nullptr)
  {
    static_assert(
      is_message_ptr_service_callback_v<Func, ServiceT> ||
        is_shared_ptr_service_callback_v<Func, ServiceT>,
      "Service callback must be invocable with "
      "(NEBULA_SERVER_REQUEST_PTR(ServiceT), NEBULA_SERVER_RESPONSE_PTR(ServiceT)) or with "
      "(std::shared_ptr<ServiceT::Request>, std::shared_ptr<ServiceT::Response>).");
  }

  // ===== Timer =====
  template <typename DurationRepT = int64_t, typename DurationT = std::milli, typename CallbackT>
  rclcpp::TimerBase::SharedPtr create_wall_timer(
    std::chrono::duration<DurationRepT, DurationT> period, CallbackT && callback,
    rclcpp::CallbackGroup::SharedPtr group = nullptr)
  {
    return node_->create_wall_timer(period, std::forward<CallbackT>(callback), group);
  }

  // ===== Internal node access =====
  // No get_agnocast_node() here: that backend does not exist in this build, matching the
  // curated surface (calling it under ENABLE_AGNOCAST=0 is a compile error, by design).
  std::shared_ptr<rclcpp::Node> get_rclcpp_node() const { return node_; }

private:
  std::shared_ptr<rclcpp::Node> node_;
};

template <typename T>
std::shared_ptr<rclcpp::Node> to_rclcpp_node(const std::shared_ptr<T> & node)
{
  return node->get_rclcpp_node();
}

template <typename CallbackT>
rclcpp::TimerBase::SharedPtr create_timer(
  Node * node, rclcpp::Clock::SharedPtr clock, rclcpp::Duration period, CallbackT && callback,
  rclcpp::CallbackGroup::SharedPtr group = nullptr)
{
  return rclcpp::create_timer(
    node->get_rclcpp_node().get(), clock, period, std::forward<CallbackT>(callback), group);
}

}  // namespace nebula::agnocast_wrapper

#endif