diff --git a/system/autoware_topic_relay_controller/CMakeLists.txt b/system/autoware_topic_relay_controller/CMakeLists.txt
new file mode 100644
index 0000000000000..f244bf8122f82
--- /dev/null
+++ b/system/autoware_topic_relay_controller/CMakeLists.txt
@@ -0,0 +1,19 @@
+cmake_minimum_required(VERSION 3.14)
+project(autoware_topic_relay_controller)
+
+find_package(autoware_cmake REQUIRED)
+autoware_package()
+
+ament_auto_add_library(${PROJECT_NAME} SHARED
+ src/topic_relay_controller_node.cpp
+)
+
+rclcpp_components_register_node(${PROJECT_NAME}
+ PLUGIN "autoware::topic_relay_controller::TopicRelayController"
+ EXECUTABLE ${PROJECT_NAME}_node
+ EXECUTOR MultiThreadedExecutor
+)
+
+ament_auto_package(INSTALL_TO_SHARE
+ launch
+)
diff --git a/system/autoware_topic_relay_controller/README.md b/system/autoware_topic_relay_controller/README.md
new file mode 100644
index 0000000000000..1976a75dc6d48
--- /dev/null
+++ b/system/autoware_topic_relay_controller/README.md
@@ -0,0 +1,15 @@
+# topic_relay_controller
+
+## Purpose
+
+## Inputs / Outputs
+
+### Input
+
+### Output
+
+## Parameters
+
+## Assumptions / Known limits
+
+TBD.
diff --git a/system/autoware_topic_relay_controller/launch/topic_relay_controller.launch.xml b/system/autoware_topic_relay_controller/launch/topic_relay_controller.launch.xml
new file mode 100644
index 0000000000000..ae642dad39f93
--- /dev/null
+++ b/system/autoware_topic_relay_controller/launch/topic_relay_controller.launch.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/system/autoware_topic_relay_controller/launch/topic_relay_controller_tf.launch.xml b/system/autoware_topic_relay_controller/launch/topic_relay_controller_tf.launch.xml
new file mode 100644
index 0000000000000..648d53cf4e1b7
--- /dev/null
+++ b/system/autoware_topic_relay_controller/launch/topic_relay_controller_tf.launch.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/system/autoware_topic_relay_controller/package.xml b/system/autoware_topic_relay_controller/package.xml
new file mode 100644
index 0000000000000..537774e67b998
--- /dev/null
+++ b/system/autoware_topic_relay_controller/package.xml
@@ -0,0 +1,24 @@
+
+
+
+ autoware_topic_relay_controller
+ 0.1.0
+ The topic_relay_controller ROS 2 package
+ Tetsuhiro Kawaguchi
+ Apache License 2.0
+
+ ament_cmake_auto
+ autoware_cmake
+
+ rclcpp
+ rclcpp_components
+ tf2_msgs
+ tier4_system_msgs
+
+ ament_lint_auto
+ autoware_lint_common
+
+
+ ament_cmake
+
+
diff --git a/system/autoware_topic_relay_controller/src/topic_relay_controller_node.cpp b/system/autoware_topic_relay_controller/src/topic_relay_controller_node.cpp
new file mode 100644
index 0000000000000..b68d8262de24d
--- /dev/null
+++ b/system/autoware_topic_relay_controller/src/topic_relay_controller_node.cpp
@@ -0,0 +1,119 @@
+// 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.
+
+#include "topic_relay_controller_node.hpp"
+
+#include
+#include
+
+namespace autoware::topic_relay_controller
+{
+TopicRelayController::TopicRelayController(const rclcpp::NodeOptions & options)
+: Node("topic_relay_controller", options), is_relaying_(true)
+{
+ // Parameter
+ node_param_.topic = declare_parameter("topic");
+ node_param_.remap_topic = declare_parameter("remap_topic");
+ node_param_.qos = declare_parameter("qos", 1);
+ node_param_.transient_local = declare_parameter("transient_local", false);
+ node_param_.best_effort = declare_parameter("best_effort", false);
+ node_param_.is_transform = (node_param_.topic == "/tf" || node_param_.topic == "/tf_static");
+ node_param_.enable_relay_control = declare_parameter("enable_relay_control");
+ node_param_.srv_name = declare_parameter("srv_name");
+ node_param_.enable_keep_publishing = declare_parameter("enable_keep_publishing");
+ node_param_.update_rate = declare_parameter("update_rate");
+
+ if (node_param_.is_transform) {
+ node_param_.frame_id = declare_parameter("frame_id");
+ node_param_.child_frame_id = declare_parameter("child_frame_id");
+ } else {
+ node_param_.topic_type = declare_parameter("topic_type");
+ }
+
+ // Service
+ if (node_param_.enable_relay_control) {
+ srv_change_relay_control_ = create_service(
+ node_param_.srv_name,
+ [this](
+ const tier4_system_msgs::srv::ChangeTopicRelayControl::Request::SharedPtr request,
+ tier4_system_msgs::srv::ChangeTopicRelayControl::Response::SharedPtr response) {
+ is_relaying_ = request->relay_on;
+ RCLCPP_INFO(get_logger(), "relay control: %s", is_relaying_ ? "ON" : "OFF");
+ response->status.success = true;
+ });
+ }
+
+ // Subscriber
+ rclcpp::QoS qos = rclcpp::QoS{node_param_.qos};
+ if (node_param_.transient_local) {
+ qos.transient_local();
+ }
+ if (node_param_.best_effort) {
+ qos.best_effort();
+ }
+
+ if (node_param_.is_transform) {
+ // Publisher
+ pub_transform_ = this->create_publisher(node_param_.remap_topic, qos);
+
+ sub_transform_ = this->create_subscription(
+ node_param_.topic, qos, [this](tf2_msgs::msg::TFMessage::SharedPtr msg) {
+ for (const auto & transform : msg->transforms) {
+ if (
+ transform.header.frame_id != node_param_.frame_id ||
+ transform.child_frame_id != node_param_.child_frame_id || !is_relaying_)
+ return;
+
+ if (node_param_.enable_keep_publishing) {
+ last_tf_topic_ = msg;
+ } else {
+ pub_transform_->publish(*msg);
+ }
+ }
+ });
+ } else {
+ // Publisher
+ pub_topic_ =
+ this->create_generic_publisher(node_param_.remap_topic, node_param_.topic_type, qos);
+
+ sub_topic_ = this->create_generic_subscription(
+ node_param_.topic, node_param_.topic_type, qos,
+ [this]([[maybe_unused]] std::shared_ptr msg) {
+ if (!is_relaying_) return;
+
+ if (node_param_.enable_keep_publishing) {
+ last_topic_ = msg;
+ } else {
+ pub_topic_->publish(*msg);
+ }
+ });
+ }
+
+ // Timer
+ if (node_param_.enable_keep_publishing) {
+ const auto update_period_ns = rclcpp::Rate(node_param_.update_rate).period();
+ timer_ = rclcpp::create_timer(this, get_clock(), update_period_ns, [this]() {
+ if (!is_relaying_) return;
+
+ if (node_param_.is_transform) {
+ if (last_tf_topic_) pub_transform_->publish(*last_tf_topic_);
+ } else {
+ if (last_topic_) pub_topic_->publish(*last_topic_);
+ }
+ });
+ }
+}
+} // namespace autoware::topic_relay_controller
+
+#include
+RCLCPP_COMPONENTS_REGISTER_NODE(autoware::topic_relay_controller::TopicRelayController)
diff --git a/system/autoware_topic_relay_controller/src/topic_relay_controller_node.hpp b/system/autoware_topic_relay_controller/src/topic_relay_controller_node.hpp
new file mode 100644
index 0000000000000..c5e2ac08203c7
--- /dev/null
+++ b/system/autoware_topic_relay_controller/src/topic_relay_controller_node.hpp
@@ -0,0 +1,77 @@
+// 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.
+
+#ifndef TOPIC_RELAY_CONTROLLER_NODE_HPP_
+#define TOPIC_RELAY_CONTROLLER_NODE_HPP_
+
+// ROS 2 core
+#include
+
+#include
+#include
+
+#include
+#include
+
+namespace autoware::topic_relay_controller
+{
+struct NodeParam
+{
+ std::string topic;
+ std::string remap_topic;
+ std::string topic_type;
+ size_t qos;
+ std::string frame_id;
+ std::string child_frame_id;
+ bool transient_local;
+ bool best_effort;
+ bool is_transform;
+ bool enable_relay_control;
+ std::string srv_name;
+ bool enable_keep_publishing;
+ int update_rate;
+};
+
+class TopicRelayController : public rclcpp::Node
+{
+public:
+ explicit TopicRelayController(const rclcpp::NodeOptions & options);
+
+private:
+ // Parameter
+ NodeParam node_param_;
+
+ // Subscriber
+ rclcpp::GenericSubscription::SharedPtr sub_topic_;
+ rclcpp::Subscription::SharedPtr sub_transform_;
+
+ // Publisher
+ rclcpp::GenericPublisher::SharedPtr pub_topic_;
+ rclcpp::Publisher::SharedPtr pub_transform_;
+
+ // Service
+ rclcpp::Service::SharedPtr
+ srv_change_relay_control_;
+
+ // Timer
+ rclcpp::TimerBase::SharedPtr timer_;
+
+ // State
+ bool is_relaying_;
+ tf2_msgs::msg::TFMessage::SharedPtr last_tf_topic_;
+ std::shared_ptr last_topic_;
+};
+} // namespace autoware::topic_relay_controller
+
+#endif // TOPIC_RELAY_CONTROLLER_NODE_HPP_