diff --git a/src/signage/config/signage_param.yaml b/src/signage/config/signage_param.yaml index e5d57df..41661b6 100644 --- a/src/signage/config/signage_param.yaml +++ b/src/signage/config/signage_param.yaml @@ -10,3 +10,13 @@ signage: accept_start: 5.0 # second monitor_width: 1920 monitor_height: 540 + announce: + emergency: true + restart_engage: true + door_close: true + door_open: true + engage: true + thank_you: true + in_emergency: true + going_to_depart: true + going_to_arrive: true \ No newline at end of file diff --git a/src/signage/package.xml b/src/signage/package.xml index b60e554..ddcd027 100644 --- a/src/signage/package.xml +++ b/src/signage/package.xml @@ -11,11 +11,13 @@ signage_version autoware_auto_system_msgs + python-pulsectl-pip rclpy tier4_api_msgs tier4_debug_msgs tier4_external_api_msgs tier4_hmi_msgs + diagnostic_updater ament_python diff --git a/src/signage/resource/sound/arrived.wav b/src/signage/resource/sound/arrived.wav deleted file mode 100644 index 0fb821a..0000000 Binary files a/src/signage/resource/sound/arrived.wav and /dev/null differ diff --git a/src/signage/resource/sound/restart_engage.wav b/src/signage/resource/sound/restart_engage.wav new file mode 100644 index 0000000..96a52ff Binary files /dev/null and b/src/signage/resource/sound/restart_engage.wav differ diff --git a/src/signage/resource/sound/wait_for_oncoming_car.wav b/src/signage/resource/sound/wait_for_oncoming_car.wav deleted file mode 100644 index 200089c..0000000 Binary files a/src/signage/resource/sound/wait_for_oncoming_car.wav and /dev/null differ diff --git a/src/signage/resource/sound/wait_for_walker.wav b/src/signage/resource/sound/wait_for_walker.wav deleted file mode 100644 index 1d9ea17..0000000 Binary files a/src/signage/resource/sound/wait_for_walker.wav and /dev/null differ diff --git a/src/signage/setup.py b/src/signage/setup.py index 2a89093..875085c 100644 --- a/src/signage/setup.py +++ b/src/signage/setup.py @@ -42,6 +42,7 @@ def package_files(directory): ("share/" + package_name, ["package.xml"]), ("share/" + package_name + "/launch", ["launch/signage.launch.xml"]), ("share/" + package_name + "/config", ["config/signage_param.yaml"]), + ("share/" + package_name + "/config", ["config/announce_settings.yaml"]), ], install_requires=["setuptools"], zip_safe=True, diff --git a/src/signage/src/signage/announce_controller.py b/src/signage/src/signage/announce_controller.py index aeeff49..63d85ca 100644 --- a/src/signage/src/signage/announce_controller.py +++ b/src/signage/src/signage/announce_controller.py @@ -2,10 +2,14 @@ # -*- coding: utf-8 -*- # This Python file uses the following encoding: utf-8 +import os + from PyQt5.QtMultimedia import QSound from rclpy.duration import Duration from ament_index_python.packages import get_package_share_directory +from dataclasses import asdict + # The higher the value, the higher the priority PRIORITY_DICT = { "emergency": 3, @@ -20,6 +24,8 @@ "going_to_arrive": 1, } +CURRENT_VOLUME_PATH = "/opt/autoware/volume.txt" + class AnnounceControllerProperty: def __init__(self, node, autoware_interface, parameter_interface): @@ -27,6 +33,7 @@ def __init__(self, node, autoware_interface, parameter_interface): self._node = node self._parameter = parameter_interface.parameter + self._announce_settings = parameter_interface.announce_settings self._current_announce = "" self._pending_announce_list = [] self._sound = QSound("") @@ -34,6 +41,18 @@ def __init__(self, node, autoware_interface, parameter_interface): self._package_path = get_package_share_directory("signage") + "/resource/sound/" self._check_playing_timer = self._node.create_timer(1, self.check_playing_callback) + self._pulse = Pulse() + if os.path.isfile(CURRENT_VOLUME_PATH): + with open(CURRENT_VOLUME_PATH, "r") as f: + self._sink = self._pulse.get_sink_by_name( + self._pulse.server_info().default_sink_name + ) + self._pulse.volume_set_all_chans(self._sink, float(f.readline())) + + self._get_volume_pub = self._node.create_publisher(Float32, "~/get/volume", 1) + self._node.create_timer(1.0, self.publish_volume_callback) + self._node.create_service(SetVolume, "~/set/volume", self.set_volume) + def process_pending_announce(self): try: for play_sound in self._pending_announce_list: @@ -62,10 +81,21 @@ def play_sound(self, message): self._sound = QSound(self._package_path + message + ".wav") self._sound.play() + # skip announce by setting + def check_announce_or_not(self, message): + try: + return asdict(self._announce_settings).get(message, False) + except Exception as e: + self._node.get_logger().error("check announce or not: " + str(e)) + return False + def send_announce(self, message): priority = PRIORITY_DICT.get(message, 0) previous_priority = PRIORITY_DICT.get(self._current_announce, 0) + if not self.check_announce_or_not(message): + return + if priority == 3: self._sound.stop() self.play_sound(message) @@ -100,3 +130,18 @@ def announce_going_to_depart_and_arrive(self, message): # To stop repeat announcement self.send_announce(message) self._prev_depart_and_arrive_type = message + + def publish_volume_callback(self): + self._sink = self._pulse.get_sink_by_name(self._pulse.server_info().default_sink_name) + self._get_volume_pub.publish(Float32(data=self._sink.volume.value_flat)) + + def set_volume(self, request, response): + try: + self._sink = self._pulse.get_sink_by_name(self._pulse.server_info().default_sink_name) + self._pulse.volume_set_all_chans(self._sink, request.volume) + with open(CURRENT_VOLUME_PATH, "w") as f: + f.write(f"{self._sink.volume.value_flat}\n") + response.status.code = ResponseStatus.SUCCESS + except Exception: + response.status.code = ResponseStatus.ERROR + return response diff --git a/src/signage/src/signage/autoware_diagnostic.py b/src/signage/src/signage/autoware_diagnostic.py new file mode 100644 index 0000000..ee8650a --- /dev/null +++ b/src/signage/src/signage/autoware_diagnostic.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020 Tier IV, Inc. All rights reserved. +# +# 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. + +# ======================================================================== +# This code is used to public the diagnostic to autoware and let autoware +# to decide the hazard level +# ======================================================================== + +import diagnostic_updater + +class AutowareDiagnostic(): + def init_updater(self, node, name, update_function, hardware_id): + updater = diagnostic_updater.Updater(node, 1) + updater.setHardwareID(hardware_id) + updater.add(name, update_function) + return updater diff --git a/src/signage/src/signage/autoware_interface.py b/src/signage/src/signage/autoware_interface.py index 5152b2c..3286249 100644 --- a/src/signage/src/signage/autoware_interface.py +++ b/src/signage/src/signage/autoware_interface.py @@ -9,12 +9,12 @@ OperationModeState, MotionState, LocalizationInitializationState, + VelocityFactorArray, ) import signage.signage_utils as utils from tier4_debug_msgs.msg import Float64Stamped from tier4_external_api_msgs.msg import DoorStatus - @dataclass class AutowareInformation: autoware_control: bool = False @@ -31,6 +31,7 @@ class AutowareInterface: def __init__(self, node): self._node = node self.information = AutowareInformation() + self.is_disconnected = False sub_qos = rclpy.qos.QoSProfile( history=rclpy.qos.QoSHistoryPolicy.KEEP_LAST, @@ -81,6 +82,12 @@ def __init__(self, node): self.sub_localization_initialization_state_callback, api_qos, ) + self._sub_velocity_factors = node.create_subscription( + VelocityFactorArray, + "/api/planning/velocity_factors", + self.sub_velocity_factors_callback, + sub_qos, + ) self._autoware_connection_time = self._node.get_clock().now() self._node.create_timer(2, self.reset_timer) @@ -88,6 +95,9 @@ def reset_timer(self): if utils.check_timeout(self._node.get_clock().now(), self._autoware_connection_time, 10): self.information = AutowareInformation() self._node.get_logger().error("Autoware disconnected", throttle_duration_sec=10) + self.is_disconnected = True + else: + self.is_disconnected = False def sub_operation_mode_callback(self, msg): try: @@ -116,7 +126,6 @@ def sub_vehicle_door_callback(self, msg): def sub_path_distance_callback(self, msg): try: - self._autoware_connection_time = self._node.get_clock().now() self.information.goal_distance = msg.data except Exception as e: self._node.get_logger().error("Unable to get the goal distance, ERROR: " + str(e)) @@ -134,3 +143,9 @@ def sub_localization_initialization_state_callback(self, msg): self._node.get_logger().error( "Unable to get the localization init state, ERROR: " + str(e) ) + + def sub_velocity_factors_callback(self, msg): + try: + self._autoware_connection_time = self._node.get_clock().now() + except Exception as e: + self._node.get_logger().error("Unable to get the velocity factors, ERROR: " + str(e)) diff --git a/src/signage/src/signage/heartbeat.py b/src/signage/src/signage/heartbeat.py new file mode 100644 index 0000000..7c83269 --- /dev/null +++ b/src/signage/src/signage/heartbeat.py @@ -0,0 +1,20 @@ +# !/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from signage.autoware_diagnostic import AutowareDiagnostic +from diagnostic_msgs.msg import DiagnosticStatus + +class Heartbeat: + def __init__(self, node): + self._node = node + + self._diagnostic_updater = AutowareDiagnostic().init_updater( + self._node, + "/system/signage_connection : signage heartbeat", + self.handle_heartbeat_diagnostics, + "none", + ) + + def handle_heartbeat_diagnostics(self, stat): + stat.summary(DiagnosticStatus.OK, "signage is working") + return stat \ No newline at end of file diff --git a/src/signage/src/signage/parameter_interface.py b/src/signage/src/signage/parameter_interface.py index a8ead50..9a50b6a 100644 --- a/src/signage/src/signage/parameter_interface.py +++ b/src/signage/src/signage/parameter_interface.py @@ -18,10 +18,23 @@ class SignageParameter: monitor_width: int = 1920 monitor_height: int = 540 +@dataclass +class AnnounceParameter: + emergency: bool = True + restart_engage: bool = True + door_close: bool = True + door_open: bool = True + engage: bool = True + arrived: bool = True + thank_you: bool = True + in_emergency: bool = True + going_to_depart: bool = True + going_to_arrive: bool = True class ParameterInterface: def __init__(self, node): self.parameter = SignageParameter() + self.announce_settings = AnnounceParameter() node.declare_parameter("signage_stand_alone", False) node.declare_parameter("ignore_manual_driving", False) @@ -64,3 +77,23 @@ def __init__(self, node): self.parameter.monitor_height = ( node.get_parameter("monitor_height").get_parameter_value().integer_value ) + + node.declare_parameter("announce.emergency", True) + node.declare_parameter("announce.restart_engage", True) + node.declare_parameter("announce.door_close", True) + node.declare_parameter("announce.door_open", True) + node.declare_parameter("announce.engage", True) + node.declare_parameter("announce.thank_you", True) + node.declare_parameter("announce.in_emergency", True) + node.declare_parameter("announce.going_to_depart", True) + node.declare_parameter("gannounce.oing_to_arrive", True) + + announce_prefix = node.get_parameters_by_prefix("announce") + + for key in announce_prefix.keys(): + setattr( + self.announce_settings, + key, + announce_prefix[key].get_parameter_value().bool_value, + ) + diff --git a/src/signage/src/signage/route_handler.py b/src/signage/src/signage/route_handler.py index 2025054..f714b2e 100644 --- a/src/signage/src/signage/route_handler.py +++ b/src/signage/src/signage/route_handler.py @@ -141,7 +141,7 @@ def announce_engage_when_starting(self): self._engage_trigger_time, self._parameter.accept_start, ): - self._announce_interface.send_announce("engage") + self._announce_interface.send_announce("restart_engage") self._engage_trigger_time = self._node.get_clock().now() if self._autoware.information.motion_state == MotionState.STARTING: @@ -370,7 +370,9 @@ def view_mode_callback(self): self._viewController.next_station_list = self._display_details.next_station_list self._viewController.display_phrase = self._display_phrase - if ( + if self._autoware.is_disconnected: + view_mode = "emergency_stopped" + elif ( not self._autoware.information.autoware_control and not self._parameter.ignore_manual_driving ): diff --git a/src/signage/src/signage/signage.py b/src/signage/src/signage/signage.py index 146e666..71f427a 100644 --- a/src/signage/src/signage/signage.py +++ b/src/signage/src/signage/signage.py @@ -7,6 +7,7 @@ import rclpy from rclpy.node import Node +from signage.heartbeat import Heartbeat from signage.view_controller import ViewControllerProperty from signage.announce_controller import AnnounceControllerProperty from signage.autoware_interface import AutowareInterface @@ -25,6 +26,8 @@ def main(args=None): app = QApplication(sys.argv) engine = QQmlApplicationEngine() + heartbeat = Heartbeat(node) + autoware_interface = AutowareInterface(node) autoware_interface = AutowareInterface(node) parameter_interface = ParameterInterface(node) ros_service_interface = RosServiceInterface(node, parameter_interface)