Skip to content

feat: manual controller #229

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
9 changes: 9 additions & 0 deletions system/rqt_autoware_manual_controller/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
1 change: 1 addition & 0 deletions system/rqt_autoware_manual_controller/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# rqt_autoware_manual_controller
26 changes: 26 additions & 0 deletions system/rqt_autoware_manual_controller/package.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>rqt_autoware_manual_controller</name>
<version>0.1.0</version>
<description>The rqt_autoware_manual_controller package</description>
<maintainer email="isamu.takagi@tier4.jp">Takagi, Isamu</maintainer>
<license>Apache License 2.0</license>

<buildtool_depend>ament_cmake_auto</buildtool_depend>
<buildtool_depend>autoware_cmake</buildtool_depend>

<exec_depend>autoware_adapi_v1_msgs</exec_depend>
<exec_depend>python_qt_binding</exec_depend>
<exec_depend>rclpy</exec_depend>
<exec_depend>rqt_gui</exec_depend>
<exec_depend>rqt_gui_py</exec_depend>

<test_depend>ament_lint_auto</test_depend>
<test_depend>autoware_lint_common</test_depend>

<export>
<build_type>ament_cmake</build_type>
<rqt_gui plugin="${prefix}/plugin.xml"/>
</export>
</package>
16 changes: 16 additions & 0 deletions system/rqt_autoware_manual_controller/plugin.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<library path="python">
<class name="AutowareManualController" type="rqt_autoware_manual_controller.ControllerPlugin" base_class_type="rqt_gui_py::Plugin">
<description>
</description>
<qtgui>
<group>
<label>Robot Tools</label>
<icon type="theme">folder</icon>
<statustip></statustip>
</group>
<label>Autoware Manual Controller</label>
<icon type="theme">utilities-system-monitor</icon>
<statustip></statustip>
</qtgui>
</class>
</library>
34 changes: 34 additions & 0 deletions system/rqt_autoware_manual_controller/python/__init__.py
Original file line number Diff line number Diff line change
@@ -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"))
13 changes: 13 additions & 0 deletions system/rqt_autoware_manual_controller/python/modules/__init__,py
Original file line number Diff line number Diff line change
@@ -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.
203 changes: 203 additions & 0 deletions system/rqt_autoware_manual_controller/python/modules/adapi.py
Original file line number Diff line number Diff line change
@@ -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)
35 changes: 35 additions & 0 deletions system/rqt_autoware_manual_controller/python/modules/gear.py
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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)
Loading
Loading