diff --git a/system/rqt_autoware_manual_controller/CMakeLists.txt b/system/rqt_autoware_manual_controller/CMakeLists.txt
new file mode 100644
index 000000000..bf4577977
--- /dev/null
+++ b/system/rqt_autoware_manual_controller/CMakeLists.txt
@@ -0,0 +1,9 @@
+cmake_minimum_required(VERSION 3.14)
+project(rqt_autoware_manual_controller)
+
+find_package(autoware_cmake REQUIRED)
+autoware_package()
+ament_python_install_package(${PROJECT_NAME} PACKAGE_DIR python)
+install(FILES plugin.xml DESTINATION share/${PROJECT_NAME})
+install(PROGRAMS script/rqt_autoware_manual_controller DESTINATION lib/${PROJECT_NAME})
+ament_auto_package(INSTALL_TO_SHARE script)
diff --git a/system/rqt_autoware_manual_controller/README.md b/system/rqt_autoware_manual_controller/README.md
new file mode 100644
index 000000000..dfcd7928a
--- /dev/null
+++ b/system/rqt_autoware_manual_controller/README.md
@@ -0,0 +1 @@
+# rqt_autoware_manual_controller
diff --git a/system/rqt_autoware_manual_controller/package.xml b/system/rqt_autoware_manual_controller/package.xml
new file mode 100644
index 000000000..36bf62dbe
--- /dev/null
+++ b/system/rqt_autoware_manual_controller/package.xml
@@ -0,0 +1,26 @@
+
+
+
+ rqt_autoware_manual_controller
+ 0.1.0
+ The rqt_autoware_manual_controller package
+ Takagi, Isamu
+ Apache License 2.0
+
+ ament_cmake_auto
+ autoware_cmake
+
+ autoware_adapi_v1_msgs
+ python_qt_binding
+ rclpy
+ rqt_gui
+ rqt_gui_py
+
+ ament_lint_auto
+ autoware_lint_common
+
+
+ ament_cmake
+
+
+
diff --git a/system/rqt_autoware_manual_controller/plugin.xml b/system/rqt_autoware_manual_controller/plugin.xml
new file mode 100644
index 000000000..b38568c8d
--- /dev/null
+++ b/system/rqt_autoware_manual_controller/plugin.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+ folder
+
+
+
+ utilities-system-monitor
+
+
+
+
diff --git a/system/rqt_autoware_manual_controller/python/__init__.py b/system/rqt_autoware_manual_controller/python/__init__.py
new file mode 100644
index 000000000..ef3745916
--- /dev/null
+++ b/system/rqt_autoware_manual_controller/python/__init__.py
@@ -0,0 +1,34 @@
+# Copyright 2025 The Autoware Contributors
+#
+# 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.
+
+from rqt_autoware_manual_controller.modules.adapi import Adapi
+from rqt_autoware_manual_controller.widget import ControllerWidget
+from rqt_gui_py.plugin import Plugin
+
+
+class ControllerPlugin(Plugin):
+ def __init__(self, context):
+ super().__init__(context)
+ self.widget = ControllerWidget(Adapi(context.node, "local"))
+ context.add_widget(self.widget)
+
+ def shutdown_plugin(self):
+ self.widget.shutdown()
+
+ def save_settings(self, plugin_settings, instance_settings):
+ plugin_settings.set_value("SplitterState", self.widget.saveState())
+
+ def restore_settings(self, plugin_settings, instance_settings):
+ if plugin_settings.contains("SplitterState"):
+ self.widget.restoreState(plugin_settings.value("SplitterState"))
diff --git a/system/rqt_autoware_manual_controller/python/modules/__init__,py b/system/rqt_autoware_manual_controller/python/modules/__init__,py
new file mode 100644
index 000000000..8004e46d2
--- /dev/null
+++ b/system/rqt_autoware_manual_controller/python/modules/__init__,py
@@ -0,0 +1,13 @@
+# Copyright 2025 The Autoware Contributors
+#
+# 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.
diff --git a/system/rqt_autoware_manual_controller/python/modules/adapi.py b/system/rqt_autoware_manual_controller/python/modules/adapi.py
new file mode 100644
index 000000000..577ab9ccf
--- /dev/null
+++ b/system/rqt_autoware_manual_controller/python/modules/adapi.py
@@ -0,0 +1,203 @@
+# Copyright 2025 The Autoware Contributors
+#
+# 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.
+
+from enum import Enum
+
+from autoware_adapi_v1_msgs.msg import Gear
+from autoware_adapi_v1_msgs.msg import GearCommand
+from autoware_adapi_v1_msgs.msg import HazardLights
+from autoware_adapi_v1_msgs.msg import HazardLightsCommand
+from autoware_adapi_v1_msgs.msg import ManualControlMode
+from autoware_adapi_v1_msgs.msg import ManualControlModeStatus
+from autoware_adapi_v1_msgs.msg import ManualOperatorHeartbeat
+from autoware_adapi_v1_msgs.msg import PedalsCommand
+from autoware_adapi_v1_msgs.msg import SteeringCommand
+from autoware_adapi_v1_msgs.msg import TurnIndicators
+from autoware_adapi_v1_msgs.msg import TurnIndicatorsCommand
+from autoware_adapi_v1_msgs.msg import VehicleStatus
+from autoware_adapi_v1_msgs.srv import ListManualControlMode
+from autoware_adapi_v1_msgs.srv import SelectManualControlMode
+from rclpy.node import Node
+from rclpy.qos import QoSDurabilityPolicy
+from rclpy.qos import QoSProfile
+from rclpy.qos import QoSReliabilityPolicy
+
+
+class ManualMode(Enum):
+ Disabled = ManualControlMode.DISABLED
+ Pedals = ManualControlMode.PEDALS
+ Acceleration = ManualControlMode.ACCELERATION
+ Velocity = ManualControlMode.VELOCITY
+
+
+class GearEnum(Enum):
+ Unknown = Gear.UNKNOWN
+ Neutral = Gear.NEUTRAL
+ Drive = Gear.DRIVE
+ Reverse = Gear.REVERSE
+ Park = Gear.PARK
+ Low = Gear.LOW
+
+
+class TurnIndicatorsEnum(Enum):
+ Unknown = TurnIndicators.UNKNOWN
+ Disable = TurnIndicators.DISABLE
+ Left = TurnIndicators.LEFT
+ Right = TurnIndicators.RIGHT
+
+
+class HazardLightsEnum(Enum):
+ Unknown = HazardLights.UNKNOWN
+ Disable = HazardLights.DISABLE
+ Enable = HazardLights.ENABLE
+
+
+class HeartbeatEnum(Enum):
+ NotReady = False
+ Ready = True
+
+
+class Adapi:
+ def __init__(self, node: Node, target: str):
+ # interfaces
+ qos_notification = QoSProfile(depth=1, durability=QoSDurabilityPolicy.TRANSIENT_LOCAL)
+ qos_realtime = QoSProfile(depth=1, reliability=QoSReliabilityPolicy.BEST_EFFORT)
+ self._node = node
+ # fmt: off
+ self._cli_mode_list = node.create_client(ListManualControlMode, f"/api/manual/{target}/control_mode/list") # noqa: E221
+ self._cli_mode_select = node.create_client(SelectManualControlMode, f"/api/manual/{target}/control_mode/select") # noqa: E221
+ self._sub_mode_status = node.create_subscription(ManualControlModeStatus, f"/api/manual/{target}/control_mode/status", self._on_mode_status, qos_notification) # noqa: E221
+ self._pub_pedals = node.create_publisher(PedalsCommand, f"/api/manual/{target}/command/pedals", qos_realtime) # noqa: E221
+ self._pub_steering = node.create_publisher(SteeringCommand, f"/api/manual/{target}/command/steering", qos_realtime) # noqa: E221
+ self._pub_gear = node.create_publisher(GearCommand, f"/api/manual/{target}/command/gear", qos_notification) # noqa: E221
+ self._pub_turn_indicators = node.create_publisher(TurnIndicatorsCommand, f"/api/manual/{target}/command/turn_indicators", qos_notification) # noqa: E221
+ self._pub_hazard_lights = node.create_publisher(HazardLightsCommand, f"/api/manual/{target}/command/hazard_lights", qos_notification) # noqa: E221
+ self._pub_heartbeat = node.create_publisher(ManualOperatorHeartbeat, f"/api/manual/{target}/operator/heartbeat", qos_realtime) # noqa: E221
+ self._sub_vehicle_status = node.create_subscription(VehicleStatus, "/api/vehicle/status", self._on_vehicle_status, qos_realtime) # noqa: E221
+ # fmt: on
+ self._timer = node.create_timer(1.0, self._on_timer)
+ self._command_timer = node.create_timer(0.1, self._on_command_timer)
+
+ # variables
+ self._mode_list = None
+ self._mode_status = None
+ self._gear_status = None
+ self._turn_indicators_status = None
+ self._hazard_lights_status = None
+ # message
+ self._msg_pedals = PedalsCommand()
+ self._msg_steering = SteeringCommand()
+ self._msg_gear = GearCommand()
+ self._msg_turn_indicators = TurnIndicatorsCommand()
+ self._msg_hazard_lights = HazardLightsCommand()
+ self._msg_heartbeat = ManualOperatorHeartbeat()
+ # callbacks
+ self._callback_mode_list = lambda _: None
+ self._callback_mode_status = lambda _: None
+ self._callback_gear_status = lambda _: None
+ self._callback_turn_indicators_status = lambda _: None
+ self._callback_hazard_lights_status = lambda _: None
+ # init
+ self.send_pedals(0.0, 0.5)
+ self.send_steering(0.0)
+ self.send_gear(GearEnum.Park)
+ self.send_turn_indicators(TurnIndicatorsEnum.Disable)
+ self.send_hazard_lights(HazardLightsEnum.Disable)
+ self.send_heartbeat(HeartbeatEnum.NotReady)
+
+ def select_mode(self, mode: ManualMode):
+ self._cli_mode_select.call_async(
+ SelectManualControlMode.Request(mode=ManualControlMode(mode=mode.value))
+ )
+
+ def set_on_mode_list(self, callback):
+ self._callback_mode_list = callback
+ if self._mode_list is not None:
+ self._callback_mode_list(self._mode_list)
+
+ def set_on_mode_status(self, callback):
+ self._callback_mode_status = callback
+ if self._mode_status is not None:
+ self._callback_mode_status(self._mode_status)
+
+ def set_on_gear_status(self, callback):
+ self._callback_gear_status = callback
+ if self._gear_status is not None:
+ self._callback_gear_status(self._gear_status)
+
+ def set_on_turn_indicators_status(self, callback):
+ self._callback_turn_indicators_status = callback
+ if self._turn_indicators_status is not None:
+ self._callback_turn_indicators_status(self._turn_indicators_status)
+
+ def set_on_hazard_lights_status(self, callback):
+ self._callback_hazard_lights_status = callback
+ if self._hazard_lights_status is not None:
+ self._callback_hazard_lights_status(self._hazard_lights_status)
+
+ def _on_mode_list(self, future):
+ result = future.result()
+ self._mode_list = [ManualMode(mode.mode) for mode in result.modes]
+ self._callback_mode_list(self._mode_list)
+
+ def _on_mode_status(self, status):
+ self._mode_status = ManualMode(status.mode.mode)
+ self._callback_mode_status(self._mode_status)
+
+ def _on_timer(self):
+ if self._mode_list is None and self._cli_mode_list.service_is_ready():
+ future = self._cli_mode_list.call_async(ListManualControlMode.Request())
+ future.add_done_callback(self._on_mode_list)
+
+ def _on_vehicle_status(self, status: VehicleStatus):
+ self._gear_status = GearEnum(status.gear.status)
+ self._turn_indicators_status = TurnIndicatorsEnum(status.turn_indicators.status)
+ self._hazard_lights_status = HazardLightsEnum(status.hazard_lights.status)
+ self._callback_gear_status(self._gear_status)
+ self._callback_turn_indicators_status(self._turn_indicators_status)
+ self._callback_hazard_lights_status(self._hazard_lights_status)
+
+ def send_pedals(self, throttle: float, brake: float):
+ self._msg_pedals.throttle = throttle
+ self._msg_pedals.brake = brake
+
+ def send_steering(self, steering: float):
+ self._msg_steering.steering_tire_angle = steering
+
+ def send_gear(self, gear: GearEnum):
+ self._msg_gear.command.status = gear.value
+
+ def send_turn_indicators(self, turn_indicators: TurnIndicatorsEnum):
+ self._msg_turn_indicators.command.status = turn_indicators.value
+
+ def send_hazard_lights(self, hazard_lights: HazardLightsEnum):
+ self._msg_hazard_lights.command.status = hazard_lights.value
+
+ def send_heartbeat(self, ready: HeartbeatEnum):
+ self._msg_heartbeat.ready = ready.value
+
+ def _on_command_timer(self):
+ stamp = self._node.get_clock().now().to_msg()
+ self._msg_pedals.stamp = stamp
+ self._msg_steering.stamp = stamp
+ self._msg_gear.stamp = stamp
+ self._msg_turn_indicators.stamp = stamp
+ self._msg_hazard_lights.stamp = stamp
+ self._msg_heartbeat.stamp = stamp
+ self._pub_pedals.publish(self._msg_pedals)
+ self._pub_steering.publish(self._msg_steering)
+ self._pub_gear.publish(self._msg_gear)
+ self._pub_turn_indicators.publish(self._msg_turn_indicators)
+ self._pub_hazard_lights.publish(self._msg_hazard_lights)
+ self._pub_heartbeat.publish(self._msg_heartbeat)
diff --git a/system/rqt_autoware_manual_controller/python/modules/gear.py b/system/rqt_autoware_manual_controller/python/modules/gear.py
new file mode 100644
index 000000000..74b7da732
--- /dev/null
+++ b/system/rqt_autoware_manual_controller/python/modules/gear.py
@@ -0,0 +1,35 @@
+# Copyright 2025 The Autoware Contributors
+#
+# 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.
+
+from python_qt_binding import QtWidgets
+from rqt_autoware_manual_controller.modules.adapi import Adapi
+from rqt_autoware_manual_controller.modules.adapi import GearEnum
+
+
+class GearControl:
+ def __init__(self, adapi: Adapi):
+ self.adapi = adapi
+ self.adapi.set_on_gear_status(self.on_gear_status)
+
+ self.command = QtWidgets.QVBoxLayout()
+ self.status = QtWidgets.QLabel()
+
+ gears = [GearEnum.Drive, GearEnum.Reverse, GearEnum.Park, GearEnum.Neutral]
+ self.buttons = {gear: QtWidgets.QPushButton(gear.name) for gear in gears}
+ for gear, button in self.buttons.items():
+ button.clicked.connect(lambda _, gear=gear: self.adapi.send_gear(gear))
+ self.command.addWidget(button)
+
+ def on_gear_status(self, gear: GearEnum):
+ self.status.setText(gear.name)
diff --git a/system/rqt_autoware_manual_controller/python/modules/hazard_lights.py b/system/rqt_autoware_manual_controller/python/modules/hazard_lights.py
new file mode 100644
index 000000000..0a4888dc5
--- /dev/null
+++ b/system/rqt_autoware_manual_controller/python/modules/hazard_lights.py
@@ -0,0 +1,39 @@
+# Copyright 2025 The Autoware Contributors
+#
+# 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.
+
+from python_qt_binding import QtWidgets
+from rqt_autoware_manual_controller.modules.adapi import Adapi
+from rqt_autoware_manual_controller.modules.adapi import HazardLightsEnum
+
+
+class HazardLightsControl:
+ def __init__(self, adapi: Adapi):
+ self.adapi = adapi
+ self.adapi.set_on_hazard_lights_status(self.on_hazard_lights_status)
+
+ self.command = QtWidgets.QVBoxLayout()
+ self.status = QtWidgets.QLabel()
+
+ hazard_lights = [HazardLightsEnum.Disable, HazardLightsEnum.Enable]
+ self.buttons = {
+ hazard_light: QtWidgets.QPushButton(hazard_light.name) for hazard_light in hazard_lights
+ }
+ for hazard_light, button in self.buttons.items():
+ button.clicked.connect(
+ lambda _, hazard_light=hazard_light: self.adapi.send_hazard_lights(hazard_light)
+ )
+ self.command.addWidget(button)
+
+ def on_hazard_lights_status(self, hazard_lights: HazardLightsEnum):
+ self.status.setText(hazard_lights.name)
diff --git a/system/rqt_autoware_manual_controller/python/modules/heartbeat.py b/system/rqt_autoware_manual_controller/python/modules/heartbeat.py
new file mode 100644
index 000000000..b1fb3091e
--- /dev/null
+++ b/system/rqt_autoware_manual_controller/python/modules/heartbeat.py
@@ -0,0 +1,30 @@
+# Copyright 2025 The Autoware Contributors
+#
+# 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.
+
+from python_qt_binding import QtWidgets
+from rqt_autoware_manual_controller.modules.adapi import Adapi
+from rqt_autoware_manual_controller.modules.adapi import HeartbeatEnum
+
+
+class HeartbeatControl:
+ def __init__(self, adapi: Adapi):
+ self.adapi = adapi
+ self.command = QtWidgets.QVBoxLayout()
+ self.status = QtWidgets.QLabel("---")
+
+ modes = [HeartbeatEnum.NotReady, HeartbeatEnum.Ready]
+ self.buttons = {mode: QtWidgets.QPushButton(mode.name) for mode in modes}
+ for mode, button in self.buttons.items():
+ button.clicked.connect(lambda _, mode=mode: self.adapi.send_heartbeat(mode))
+ self.command.addWidget(button)
diff --git a/system/rqt_autoware_manual_controller/python/modules/mode_select.py b/system/rqt_autoware_manual_controller/python/modules/mode_select.py
new file mode 100644
index 000000000..47ab54cc3
--- /dev/null
+++ b/system/rqt_autoware_manual_controller/python/modules/mode_select.py
@@ -0,0 +1,43 @@
+# Copyright 2025 The Autoware Contributors
+#
+# 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.
+
+from python_qt_binding import QtWidgets
+from rqt_autoware_manual_controller.modules.adapi import Adapi
+from rqt_autoware_manual_controller.modules.adapi import ManualMode
+
+
+class ManualModeSelect(QtWidgets.QVBoxLayout):
+ def __init__(self, adapi: Adapi):
+ super().__init__()
+ adapi.set_on_mode_list(self.on_mode_list)
+ self.adapi = adapi
+ self.buttons = {mode: QtWidgets.QPushButton(mode.name) for mode in ManualMode}
+ for mode, button in self.buttons.items():
+ button.setEnabled(mode is ManualMode.Disabled)
+ button.clicked.connect(lambda clicked, mode=mode: self.adapi.select_mode(mode))
+ self.addWidget(button)
+
+ def on_mode_list(self, modes):
+ for mode in modes:
+ self.buttons[mode].setEnabled(True)
+
+
+class ManualModeStatus(QtWidgets.QLabel):
+ def __init__(self, adapi: Adapi):
+ super().__init__()
+ self.setText("Unknown")
+ adapi.set_on_mode_status(self.on_mode_status)
+
+ def on_mode_status(self, mode: ManualMode):
+ self.setText(mode.name)
diff --git a/system/rqt_autoware_manual_controller/python/modules/mouse_control.py b/system/rqt_autoware_manual_controller/python/modules/mouse_control.py
new file mode 100644
index 000000000..4f0ad5fff
--- /dev/null
+++ b/system/rqt_autoware_manual_controller/python/modules/mouse_control.py
@@ -0,0 +1,44 @@
+from python_qt_binding import QtCore
+from python_qt_binding import QtWidgets
+from rqt_autoware_manual_controller.modules.adapi import Adapi
+
+
+class MouseControl:
+ def __init__(self, adapi: Adapi):
+ self.adapi = adapi
+ self.capture = MouseCapture(self)
+ self.capture.setText("+")
+ self.capture.setAlignment(QtCore.Qt.AlignCenter)
+ self.capture.setStyleSheet("background-color: gray;")
+ self.capture.setMouseTracking(False)
+ self.accel = QtWidgets.QLabel()
+ self.brake = QtWidgets.QLabel()
+ self.steer = QtWidgets.QLabel()
+
+ def update_pedals(self, accel: float, brake: float):
+ self.accel.setText(f"{accel:+0.2f}")
+ self.brake.setText(f"{brake:+0.2f}")
+ self.adapi.send_pedals(accel, brake)
+
+ def update_steer(self, steer: float):
+ self.steer.setText(f"{steer:+0.2f}")
+ self.adapi.send_steering(steer)
+
+
+class MouseCapture(QtWidgets.QLabel):
+ def __init__(self, control: MouseControl):
+ super().__init__()
+ self.control = control
+
+ def mouseMoveEvent(self, event):
+ w = self.size().width() / 2
+ h = self.size().height() / 2
+ x = (event.pos().x() - w) / w
+ y = (event.pos().y() - h) / h
+ x = -max(-1.0, min(1.0, x))
+ y = -max(-1.0, min(1.0, y))
+ steer = x
+ accel = max(0.0, +y)
+ brake = max(0.0, -y)
+ self.control.update_pedals(accel, brake)
+ self.control.update_steer(steer)
diff --git a/system/rqt_autoware_manual_controller/python/modules/turn_indicators.py b/system/rqt_autoware_manual_controller/python/modules/turn_indicators.py
new file mode 100644
index 000000000..8ca4981dc
--- /dev/null
+++ b/system/rqt_autoware_manual_controller/python/modules/turn_indicators.py
@@ -0,0 +1,46 @@
+# Copyright 2025 The Autoware Contributors
+#
+# 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.
+
+from python_qt_binding import QtWidgets
+from rqt_autoware_manual_controller.modules.adapi import Adapi
+from rqt_autoware_manual_controller.modules.adapi import TurnIndicatorsEnum
+
+
+class TurnIndicatorsControl:
+ def __init__(self, adapi: Adapi):
+ self.adapi = adapi
+ self.adapi.set_on_turn_indicators_status(self.on_turn_indicators_status)
+
+ self.command = QtWidgets.QVBoxLayout()
+ self.status = QtWidgets.QLabel()
+
+ turn_indicators = [
+ TurnIndicatorsEnum.Disable,
+ TurnIndicatorsEnum.Left,
+ TurnIndicatorsEnum.Right,
+ ]
+ self.buttons = {
+ turn_indicator: QtWidgets.QPushButton(turn_indicator.name)
+ for turn_indicator in turn_indicators
+ }
+ for turn_indicator, button in self.buttons.items():
+ button.clicked.connect(
+ lambda _, turn_indicator=turn_indicator: self.adapi.send_turn_indicators(
+ turn_indicator
+ )
+ )
+ self.command.addWidget(button)
+
+ def on_turn_indicators_status(self, turn_indicators: TurnIndicatorsEnum):
+ self.status.setText(turn_indicators.name)
diff --git a/system/rqt_autoware_manual_controller/python/widget.py b/system/rqt_autoware_manual_controller/python/widget.py
new file mode 100644
index 000000000..adfa378d5
--- /dev/null
+++ b/system/rqt_autoware_manual_controller/python/widget.py
@@ -0,0 +1,91 @@
+# Copyright 2025 The Autoware Contributors
+#
+# 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.
+
+from python_qt_binding import QtWidgets
+from rqt_autoware_manual_controller.modules.adapi import Adapi
+from rqt_autoware_manual_controller.modules.gear import GearControl
+from rqt_autoware_manual_controller.modules.hazard_lights import HazardLightsControl
+from rqt_autoware_manual_controller.modules.heartbeat import HeartbeatControl
+from rqt_autoware_manual_controller.modules.mode_select import ManualModeSelect
+from rqt_autoware_manual_controller.modules.mode_select import ManualModeStatus
+from rqt_autoware_manual_controller.modules.mouse_control import MouseControl
+from rqt_autoware_manual_controller.modules.turn_indicators import TurnIndicatorsControl
+
+
+class ControllerWidget(QtWidgets.QSplitter):
+ def __init__(self, adapi: Adapi):
+ super().__init__()
+ self.adapi = adapi
+ self.mode_select = ManualModeSelect(self.adapi)
+ self.mode_status = ManualModeStatus(self.adapi)
+ self.mouse = MouseControl(self.adapi)
+ self.gear = GearControl(self.adapi)
+ self.turn_indicators = TurnIndicatorsControl(self.adapi)
+ self.hazard_lights = HazardLightsControl(self.adapi)
+ self.heartbeat = HeartbeatControl(self.adapi)
+ row = 0
+ layout = QtWidgets.QGridLayout()
+ layout.addWidget(QtWidgets.QLabel("Item"), row, 0)
+ layout.addWidget(QtWidgets.QLabel("Status"), row, 1)
+ layout.addWidget(QtWidgets.QLabel("Command"), row, 2)
+ row += 1
+ layout.addWidget(QtWidgets.QLabel("Mode"), row, 0)
+ layout.addWidget(self.mode_status, row, 1)
+ layout.addLayout(self.mode_select, row, 2)
+ row += 1
+ layout.addWidget(QtWidgets.QLabel("Velocity"), row, 0)
+ layout.addWidget(QtWidgets.QLabel("---"), row, 1)
+ layout.addWidget(QtWidgets.QLabel("---"), row, 2)
+ row += 1
+ layout.addWidget(QtWidgets.QLabel("Acceleration"), row, 0)
+ layout.addWidget(QtWidgets.QLabel("---"), row, 1)
+ layout.addWidget(QtWidgets.QLabel("---"), row, 2)
+ row += 1
+ layout.addWidget(QtWidgets.QLabel("Throttle"), row, 0)
+ layout.addWidget(QtWidgets.QLabel("---"), row, 1)
+ layout.addWidget(self.mouse.accel, row, 2)
+ row += 1
+ layout.addWidget(QtWidgets.QLabel("Brake"), row, 0)
+ layout.addWidget(QtWidgets.QLabel("---"), row, 1)
+ layout.addWidget(self.mouse.brake, row, 2)
+ row += 1
+ layout.addWidget(QtWidgets.QLabel("Steering"), row, 0)
+ layout.addWidget(QtWidgets.QLabel("---"), row, 1)
+ layout.addWidget(self.mouse.steer, row, 2)
+ row += 1
+ layout.addWidget(QtWidgets.QLabel("Gear"), row, 0)
+ layout.addWidget(self.gear.status, row, 1)
+ layout.addLayout(self.gear.command, row, 2)
+ row += 1
+ layout.addWidget(QtWidgets.QLabel("Turn Indicator"), row, 0)
+ layout.addWidget(self.turn_indicators.status, row, 1)
+ layout.addLayout(self.turn_indicators.command, row, 2)
+ row += 1
+ layout.addWidget(QtWidgets.QLabel("Hazard Lights"), row, 0)
+ layout.addWidget(self.hazard_lights.status, row, 1)
+ layout.addLayout(self.hazard_lights.command, row, 2)
+ row += 1
+ layout.addWidget(QtWidgets.QLabel("Heartbeat"), row, 0)
+ layout.addWidget(self.heartbeat.status, row, 1)
+ layout.addLayout(self.heartbeat.command, row, 2)
+ row += 1
+ layout.setRowStretch(row, 1)
+
+ widget = QtWidgets.QWidget()
+ widget.setLayout(layout)
+ self.addWidget(self.mouse.capture)
+ self.addWidget(widget)
+
+ def shutdown(self):
+ pass
diff --git a/system/rqt_autoware_manual_controller/script/rqt_autoware_manual_controller b/system/rqt_autoware_manual_controller/script/rqt_autoware_manual_controller
new file mode 100755
index 000000000..8aaad4d21
--- /dev/null
+++ b/system/rqt_autoware_manual_controller/script/rqt_autoware_manual_controller
@@ -0,0 +1,8 @@
+#!/usr/bin/env python3
+
+import sys
+
+import rqt_gui.main
+
+rqt_main = rqt_gui.main.Main()
+sys.exit(rqt_main.main(sys.argv, standalone="rqt_autoware_manual_controller.ControllerPlugin"))