From dd938c8d7623ebe154eefe0571516f56a37eeb90 Mon Sep 17 00:00:00 2001 From: yk1109 <150131701+YoshihiroKogure@users.noreply.github.com> Date: Thu, 24 Oct 2024 08:54:25 +0900 Subject: [PATCH] feat(control_data_collecting_tool): add safety measures and rosbag recording and loading (#134) * add safety measures Signed-off-by: Yoshihiro Kogure * add rosbag2 recording and loading functionalities Signed-off-by: Yoshihiro Kogure * slight modifications Signed-off-by: Yoshihiro Kogure * revert unncessary chnages Signed-off-by: Yoshihiro Kogure * add a flag to determin whether to load rosbag2 data or not Signed-off-by: Yoshihiro Kogure * record rosbag only if operation_mode is 3 Signed-off-by: Yoshihiro Kogure * add some comments and made minor adjustments Signed-off-by: Yoshihiro Kogure * style(pre-commit): autofix * Update control_data_collecting_tool/scripts/data_collecting_rosbag_record.py Signed-off-by: Kosuke Takeuchi * fix typos Signed-off-by: Yoshihiro Kogure * subscribe control_mode Signed-off-by: Yoshihiro Kogure * fix typo Signed-off-by: Yoshihiro Kogure * style(pre-commit): autofix --------- Signed-off-by: Yoshihiro Kogure Signed-off-by: Kosuke Takeuchi Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Kosuke Takeuchi --- control_data_collecting_tool/CMakeLists.txt | 4 + control_data_collecting_tool/README.md | 97 +++-- .../config/param.yaml | 28 +- .../config/topics.yaml | 10 + .../control_data_collecting_tool.launch.py | 11 + .../scripts/data_collecting_base_node.py | 171 +++++++++ .../scripts/data_collecting_data_counter.py | 279 +++++++++++++++ .../scripts/data_collecting_plotter.py | 262 +++----------- .../scripts/data_collecting_rosbag_record.py | 207 +++++++++++ .../data_collecting_trajectory_publisher.py | 332 +++++++----------- .../scripts/rosbag_play.py | 96 +++++ 11 files changed, 1010 insertions(+), 487 deletions(-) create mode 100644 control_data_collecting_tool/config/topics.yaml create mode 100644 control_data_collecting_tool/scripts/data_collecting_base_node.py create mode 100755 control_data_collecting_tool/scripts/data_collecting_data_counter.py create mode 100755 control_data_collecting_tool/scripts/data_collecting_rosbag_record.py create mode 100644 control_data_collecting_tool/scripts/rosbag_play.py diff --git a/control_data_collecting_tool/CMakeLists.txt b/control_data_collecting_tool/CMakeLists.txt index 6d817fcd..e224634c 100644 --- a/control_data_collecting_tool/CMakeLists.txt +++ b/control_data_collecting_tool/CMakeLists.txt @@ -8,6 +8,10 @@ install(PROGRAMS scripts/data_collecting_pure_pursuit_trajectory_follower.py scripts/data_collecting_trajectory_publisher.py scripts/data_collecting_plotter.py + scripts/data_collecting_rosbag_record.py + scripts/data_collecting_data_counter.py + scripts/data_collecting_base_node.py + scripts/rosbag_play.py DESTINATION lib/${PROJECT_NAME} ) diff --git a/control_data_collecting_tool/README.md b/control_data_collecting_tool/README.md index 4f853eec..db8ca8d3 100644 --- a/control_data_collecting_tool/README.md +++ b/control_data_collecting_tool/README.md @@ -33,6 +33,12 @@ This package provides tools for automatically collecting data using pure pursuit ros2 launch control_data_collecting_tool control_data_collecting_tool.launch.py ``` + - Control data collecting tool automatically records topics included in `config/topics.yaml` when the above command is executed. + The topics will be saved in rosbag2 format in the directory where the above command is executed. + + - The data from `/localization/kinematic_state` and `/localization/acceleration` located in the directory (rosbag2 format) where the command is executed will be automatically loaded and reflected in the data count for these topics. + (If `LOAD_ROSBAG2_FILES` in `config/param.yaml` is set to `false`, the data is noe loaded.) + 5. Add visualization in rviz: - `/data_collecting_area` @@ -53,13 +59,7 @@ This package provides tools for automatically collecting data using pure pursuit > [!NOTE] > You cannot change the data collecting area while driving. -7. start recording rosbag data. For example, run the following command: - - ```bash - ros2 bag record /localization/kinematic_state /localization/acceleration /vehicle/status/steering_status /sensing/imu/imu_data /system/operation_mode/state /vehicle/status/control_mode /external/selected/control_cmd /external/selected/gear_cmd /data_collecting_trajectory - ``` - -8. Click the `LOCAL` button on `OperationMode` in `AutowareStatePanel`. +7. Click the `LOCAL` button on `OperationMode` in `AutowareStatePanel`. @@ -67,17 +67,20 @@ This package provides tools for automatically collecting data using pure pursuit -9. If you want to stop data collecting automatic driving, run the following command +8. If you want to stop data collecting automatic driving, run the following command ```bash ros2 topic pub /data_collecting_stop_request std_msgs/msg/Bool "data: true" --once ``` -10. If you want to restart data collecting automatic driving, run the following command + > [!NOTE] + > When the car crosses the green boundary line, a similar stopping procedure will be automatically triggered. -```bash -ros2 topic pub /data_collecting_stop_request std_msgs/msg/Bool "data: false" --once -``` +9. If you want to restart data collecting automatic driving, run the following command + + ```bash + ros2 topic pub /data_collecting_stop_request std_msgs/msg/Bool "data: false" --once + ``` ## Change Courses @@ -95,22 +98,25 @@ You can change the course by selecting `COURSE_NAME` in `config/param.yaml` from ## Parameter -ROS 2 params in `/data_collecting_trajectory_publisher` node: +ROS 2 params which are common in all nodes: | Name | Type | Description | Default value | | :--------------------------------------- | :------- | :-------------------------------------------------------------------------------------------------- | :------------- | +| `LOAD_ROSBAG2_FILES` | `bool` | Flag that determines whether to load rosbag2 data or not | True | | `COURSE_NAME` | `string` | Course name [`eight_course`, `u_shaped_return`, `straight_line_positive`, `straight_line_negative`] | `eight_course` | | `NUM_BINS_V` | `int` | Number of bins of velocity in heatmap | 10 | | `NUM_BINS_STEER` | `int` | Number of bins of steer in heatmap | 10 | -| `NUM_BINS_ACCELERATION` | `int` | Number of bins of acceleration in heatmap | 10 | +| `NUM_BINS_A` | `int` | Number of bins of acceleration in heatmap | 10 | | `V_MIN` | `double` | Minimum velocity in heatmap [m/s] | 0.0 | | `V_MAX` | `double` | Maximum velocity in heatmap [m/s] | 11.5 | | `STEER_MIN` | `double` | Minimum steer in heatmap [rad] | -1.0 | | `STEER_MAX` | `double` | Maximum steer in heatmap [rad] | 1.0 | | `A_MIN` | `double` | Minimum acceleration in heatmap [m/ss] | -1.0 | | `A_MAX` | `double` | Maximum acceleration in heatmap [m/ss] | 1.0 | -| `wheel_base` | `double` | Wheel base [m] | 2.79 | -| `acc_kp` | `double` | Accel command proportional gain | 1.0 | +| `COLLECTING_DATA_V_MIN` | `double` | Minimum velocity for data collection [m/s] | 0.0 | +| `COLLECTING_DATA_V_MAX` | `double` | Maximum velocity for data collection [m/s] | 11.5 | +| `COLLECTING_DATA_A_MIN` | `double` | Minimum velocity for data collection [m/ss] | 1.0 | +| `COLLECTING_DATA_A_MAX` | `double` | Maximum velocity for data collection [m/ss] | -1.0 | | `max_lateral_accel` | `double` | Max lateral acceleration limit [m/ss] | 0.5 | | `lateral_error_threshold` | `double` | Lateral error threshold where applying velocity limit [m/s] | 5.0 | | `yaw_error_threshold` | `double` | Yaw error threshold where applying velocity limit [rad] | 0.50 | @@ -120,43 +126,22 @@ ROS 2 params in `/data_collecting_trajectory_publisher` node: | `longitudinal_velocity_noise_amp` | `double` | Target longitudinal velocity additional sine noise amplitude [m/s] | 0.01 | | `longitudinal_velocity_noise_min_period` | `double` | Target longitudinal velocity additional sine noise minimum period [s] | 5.0 | | `longitudinal_velocity_noise_max_period` | `double` | Target longitudinal velocity additional sine noise maximum period [s] | 20.0 | - -ROS 2 params in `/data_collecting_pure_pursuit_trajectory_follower` node: - -| Name | Type | Description | Default value | -| :--------------------------------------- | :------- | :------------------------------------------------------------- | :------------ | -| `pure_pursuit_type` | `string` | Pure pursuit type (`naive` or `linearized` steer control law ) | `linearized` | -| `wheel_base` | `double` | Wheel base [m] | 2.79 | -| `acc_kp` | `double` | Accel command proportional gain | 1.0 | -| `lookahead_time` | `double` | Pure pursuit lookahead time [s] | 2.0 | -| `min_lookahead` | `double` | Pure pursuit minimum lookahead length [m] | 2.0 | -| `linearized_pure_pursuit_steer_kp_param` | `double` | Linearized pure pursuit steering P gain parameter | 2.0 | -| `linearized_pure_pursuit_steer_kd_param` | `double` | Linearized pure pursuit steering D gain parameter | 2.0 | -| `stop_acc` | `double` | Accel command for stopping data collecting driving [m/ss] | -2.0 | -| `stop_jerk_lim` | `double` | Jerk limit for stopping data collecting driving [m/sss] | 1.0 | -| `lon_acc_lim` | `double` | Longitudinal acceleration limit [m/ss] | 5.0 | -| `lon_jerk_lim` | `double` | Longitudinal jerk limit [m/sss] | 5.0 | -| `steer_lim` | `double` | Steering angle limit [rad] | 1.0 | -| `steer_rate_lim` | `double` | Steering angle rate limit [rad/s] | 1.0 | -| `acc_noise_amp` | `double` | Accel command additional sine noise amplitude [m/ss] | 0.01 | -| `acc_noise_min_period` | `double` | Accel command additional sine noise minimum period [s] | 5.0 | -| `acc_noise_max_period` | `double` | Accel command additional sine noise maximum period [s] | 20.0 | -| `steer_noise_amp` | `double` | Steer command additional sine noise amplitude [rad] | 0.01 | -| `steer_noise_max_period` | `double` | Steer command additional sine noise maximum period [s] | 5.0 | -| `steer_noise_min_period` | `double` | Steer command additional sine noise minimum period [s] | 20.0 | - -ROS 2 params in `/data_collecting_plotter` node: - -| Name | Type | Description | Default value | -| :---------------------- | :------- | :-------------------------------------------------------------------------------------------------- | :------------- | -| `COURSE_NAME` | `string` | Course name [`eight_course`, `u_shaped_return`, `straight_line_positive`, `straight_line_negative`] | `eight_course` | -| `NUM_BINS_V` | `int` | Number of bins of velocity in heatmap | 10 | -| `NUM_BINS_STEER` | `int` | Number of bins of steer in heatmap | 10 | -| `NUM_BINS_ACCELERATION` | `int` | Number of bins of acceleration in heatmap | 10 | -| `V_MIN` | `double` | Minimum velocity in heatmap [m/s] | 0.0 | -| `V_MAX` | `double` | Maximum velocity in heatmap [m/s] | 11.5 | -| `STEER_MIN` | `double` | Minimum steer in heatmap [rad] | -1.0 | -| `STEER_MAX` | `double` | Maximum steer in heatmap [rad] | 1.0 | -| `A_MIN` | `double` | Minimum acceleration in heatmap [m/ss] | -1.0 | -| `A_MAX` | `double` | Maximum acceleration in heatmap [m/ss] | 1.0 | -| `wheel_base` | `double` | Wheel base [m] | 2.79 | +| `pure_pursuit_type` | `string` | Pure pursuit type (`naive` or `linearized` steer control law ) | `linearized` | +| `wheel_base` | `double` | Wheel base [m] | 2.79 | +| `acc_kp` | `double` | Accel command proportional gain | 1.0 | +| `lookahead_time` | `double` | Pure pursuit lookahead time [s] | 2.0 | +| `min_lookahead` | `double` | Pure pursuit minimum lookahead length [m] | 2.0 | +| `linearized_pure_pursuit_steer_kp_param` | `double` | Linearized pure pursuit steering P gain parameter | 2.0 | +| `linearized_pure_pursuit_steer_kd_param` | `double` | Linearized pure pursuit steering D gain parameter | 2.0 | +| `stop_acc` | `double` | Accel command for stopping data collecting driving [m/ss] | -2.0 | +| `stop_jerk_lim` | `double` | Jerk limit for stopping data collecting driving [m/sss] | 5.0 | +| `lon_acc_lim` | `double` | Longitudinal acceleration limit [m/ss] | 5.0 | +| `lon_jerk_lim` | `double` | Longitudinal jerk limit [m/sss] | 5.0 | +| `steer_lim` | `double` | Steering angle limit [rad] | 1.0 | +| `steer_rate_lim` | `double` | Steering angle rate limit [rad/s] | 1.0 | +| `acc_noise_amp` | `double` | Accel command additional sine noise amplitude [m/ss] | 0.01 | +| `acc_noise_min_period` | `double` | Accel command additional sine noise minimum period [s] | 5.0 | +| `acc_noise_max_period` | `double` | Accel command additional sine noise maximum period [s] | 20.0 | +| `steer_noise_amp` | `double` | Steer command additional sine noise amplitude [rad] | 0.01 | +| `steer_noise_max_period` | `double` | Steer command additional sine noise maximum period [s] | 5.0 | +| `steer_noise_min_period` | `double` | Steer command additional sine noise minimum period [s] | 20.0 | diff --git a/control_data_collecting_tool/config/param.yaml b/control_data_collecting_tool/config/param.yaml index d96553ac..e14b66a8 100644 --- a/control_data_collecting_tool/config/param.yaml +++ b/control_data_collecting_tool/config/param.yaml @@ -1,19 +1,7 @@ -data_collecting_plotter: +/**: ros__parameters: - NUM_BINS_V: 10 - NUM_BINS_STEER: 10 - NUM_BINS_A: 10 - V_MIN: 0.0 - V_MAX: 11.5 - STEER_MIN: -1.0 - STEER_MAX: 1.0 - A_MIN: -1.0 - A_MAX: 1.0 - - wheel_base: 2.79 + LOAD_ROSBAG2_FILES: true -data_collecting_trajectory_publisher: - ros__parameters: COURSE_NAME: eight_course # COURSE_NAME: u_shaped_return # COURSE_NAME: straight_line_positive @@ -29,8 +17,11 @@ data_collecting_trajectory_publisher: A_MIN: -1.0 A_MAX: 1.0 - wheel_base: 2.79 - acc_kp: 1.0 + COLLECTING_DATA_V_MIN: 0.0 + COLLECTING_DATA_V_MAX: 11.5 + COLLECTING_DATA_A_MIN: -1.0 + COLLECTING_DATA_A_MAX: 1.0 + max_lateral_accel: 0.5 lateral_error_threshold: 2.0 yaw_error_threshold: 0.50 @@ -41,9 +32,8 @@ data_collecting_trajectory_publisher: longitudinal_velocity_noise_min_period: 5.0 longitudinal_velocity_noise_max_period: 20.0 -data_collecting_pure_pursuit_trajectory_follower: - ros__parameters: pure_pursuit_type: linearized + # pure_pursuit_type: naive wheel_base: 2.79 acc_kp: 1.0 lookahead_time: 2.0 @@ -51,7 +41,7 @@ data_collecting_pure_pursuit_trajectory_follower: linearized_pure_pursuit_steer_kp_param: 2.0 linearized_pure_pursuit_steer_kd_param: 2.0 stop_acc: -2.0 - stop_jerk_lim: 2.0 + stop_jerk_lim: 5.0 lon_acc_lim: 2.0 lon_jerk_lim: 5.0 steer_lim: 1.0 diff --git a/control_data_collecting_tool/config/topics.yaml b/control_data_collecting_tool/config/topics.yaml new file mode 100644 index 00000000..6fc3ec9a --- /dev/null +++ b/control_data_collecting_tool/config/topics.yaml @@ -0,0 +1,10 @@ +topics: + - /localization/kinematic_state + - /localization/acceleration + - /vehicle/status/steering_status + - /sensing/imu/imu_data + - /system/operation_mode/state + - /vehicle/status/control_mode + - /external/selected/control_cmd + - /external/selected/gear_cmd + - /data_collecting_trajectory diff --git a/control_data_collecting_tool/launch/control_data_collecting_tool.launch.py b/control_data_collecting_tool/launch/control_data_collecting_tool.launch.py index ba3ea314..475c7023 100644 --- a/control_data_collecting_tool/launch/control_data_collecting_tool.launch.py +++ b/control_data_collecting_tool/launch/control_data_collecting_tool.launch.py @@ -45,6 +45,17 @@ def generate_launch_description(): name="data_collecting_plotter", parameters=[param_file_path], ), + Node( + package="control_data_collecting_tool", + executable="data_collecting_rosbag_record.py", + name="data_collecting_rosbag_record", + ), + Node( + package="control_data_collecting_tool", + executable="data_collecting_data_counter.py", + name="data_collecting_data_counter", + parameters=[param_file_path], + ), ] ) diff --git a/control_data_collecting_tool/scripts/data_collecting_base_node.py b/control_data_collecting_tool/scripts/data_collecting_base_node.py new file mode 100644 index 00000000..885e6d7a --- /dev/null +++ b/control_data_collecting_tool/scripts/data_collecting_base_node.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 + +# Copyright 2024 Proxima Technology Inc, TIER IV +# +# 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 autoware_adapi_v1_msgs.msg import OperationModeState +from autoware_vehicle_msgs.msg import ControlModeReport +from geometry_msgs.msg import AccelWithCovarianceStamped +from nav_msgs.msg import Odometry +import numpy as np +from rcl_interfaces.msg import ParameterDescriptor +from rclpy.node import Node + + +class DataCollectingBaseNode(Node): + def __init__(self, node_name): + super().__init__(node_name) + + # common params + self.declare_parameter( + "NUM_BINS_V", + 10, + ParameterDescriptor(description="Number of bins of velocity in heatmap"), + ) + + self.declare_parameter( + "NUM_BINS_STEER", + 10, + ParameterDescriptor(description="Number of bins of steer in heatmap"), + ) + + self.declare_parameter( + "NUM_BINS_A", + 10, + ParameterDescriptor(description="Number of bins of acceleration in heatmap"), + ) + + self.declare_parameter( + "V_MIN", + 0.0, + ParameterDescriptor(description="Minimum velocity in heatmap [m/s]"), + ) + + self.declare_parameter( + "V_MAX", + 11.5, + ParameterDescriptor(description="Maximum velocity in heatmap [m/s]"), + ) + + self.declare_parameter( + "STEER_MIN", + -1.0, + ParameterDescriptor(description="Minimum steer in heatmap [rad]"), + ) + + self.declare_parameter( + "STEER_MAX", + 1.0, + ParameterDescriptor(description="Maximum steer in heatmap [rad]"), + ) + + self.declare_parameter( + "A_MIN", + -1.0, + ParameterDescriptor(description="Minimum acceleration in heatmap [m/ss]"), + ) + + self.declare_parameter( + "A_MAX", + 1.0, + ParameterDescriptor(description="Maximum acceleration in heatmap [m/ss]"), + ) + + self.declare_parameter( + "wheel_base", + 2.79, + ParameterDescriptor(description="Wheel base [m]"), + ) + + self.sub_odometry_ = self.create_subscription( + Odometry, + "/localization/kinematic_state", + self.onOdometry, + 1, + ) + + self.sub_acceleration_ = self.create_subscription( + AccelWithCovarianceStamped, + "/localization/acceleration", + self.onAcceleration, + 1, + ) + + self.operation_mode_subscription_ = self.create_subscription( + OperationModeState, + "/system/operation_mode/state", + self.subscribe_operation_mode, + 10, + ) + + self.control_mode_subscription_ = self.create_subscription( + ControlModeReport, + "/vehicle/status/control_mode", + self.subscribe_control_mode, + 10, + ) + + self._present_kinematic_state = None + self._present_acceleration = None + self.present_operation_mode_ = None + self._present_control_mode_ = None + + """ + velocity and acceleration grid + velocity and steer grid + """ + self.num_bins_v = self.get_parameter("NUM_BINS_V").get_parameter_value().integer_value + self.num_bins_steer = ( + self.get_parameter("NUM_BINS_STEER").get_parameter_value().integer_value + ) + self.num_bins_a = self.get_parameter("NUM_BINS_A").get_parameter_value().integer_value + self.v_min, self.v_max = ( + self.get_parameter("V_MIN").get_parameter_value().double_value, + self.get_parameter("V_MAX").get_parameter_value().double_value, + ) + self.steer_min, self.steer_max = ( + self.get_parameter("STEER_MIN").get_parameter_value().double_value, + self.get_parameter("STEER_MAX").get_parameter_value().double_value, + ) + self.a_min, self.a_max = ( + self.get_parameter("A_MIN").get_parameter_value().double_value, + self.get_parameter("A_MAX").get_parameter_value().double_value, + ) + + self.collected_data_counts_of_vel_acc = np.zeros( + (self.num_bins_v, self.num_bins_a), dtype=np.int32 + ) + self.collected_data_counts_of_vel_steer = np.zeros( + (self.num_bins_v, self.num_bins_steer), dtype=np.int32 + ) + + self.v_bins = np.linspace(self.v_min, self.v_max, self.num_bins_v + 1) + self.steer_bins = np.linspace(self.steer_min, self.steer_max, self.num_bins_steer + 1) + self.a_bins = np.linspace(self.a_min, self.a_max, self.num_bins_a + 1) + + self.v_bin_centers = (self.v_bins[:-1] + self.v_bins[1:]) / 2 + self.steer_bin_centers = (self.steer_bins[:-1] + self.steer_bins[1:]) / 2 + self.a_bin_centers = (self.a_bins[:-1] + self.a_bins[1:]) / 2 + + def onOdometry(self, msg): + self._present_kinematic_state = msg + + def onAcceleration(self, msg): + self._present_acceleration = msg + + def subscribe_operation_mode(self, msg): + self.present_operation_mode_ = msg.mode + + def subscribe_control_mode(self, msg): + self._present_control_mode_ = msg.mode diff --git a/control_data_collecting_tool/scripts/data_collecting_data_counter.py b/control_data_collecting_tool/scripts/data_collecting_data_counter.py new file mode 100755 index 00000000..6c441546 --- /dev/null +++ b/control_data_collecting_tool/scripts/data_collecting_data_counter.py @@ -0,0 +1,279 @@ +#!/usr/bin/env python3 + +# Copyright 2024 Proxima Technology Inc, TIER IV +# +# 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 collections import deque +import os + +from data_collecting_base_node import DataCollectingBaseNode +import numpy as np +from numpy import arctan2 +from rcl_interfaces.msg import ParameterDescriptor +import rclpy +import rosbag_play +from std_msgs.msg import Float32MultiArray +from std_msgs.msg import Int32MultiArray +from std_msgs.msg import MultiArrayDimension +from std_msgs.msg import MultiArrayLayout + + +def publish_Int32MultiArray(publisher_, array_data): + msg = Int32MultiArray() + + flattened_data = array_data.flatten().tolist() + msg.data = flattened_data + + layout = MultiArrayLayout() + + dim1 = MultiArrayDimension() + dim1.label = "rows" + dim1.size = len(array_data) + dim1.stride = len(flattened_data) + + dim2 = MultiArrayDimension() + dim2.label = "cols" + dim2.size = len(array_data[0]) + dim2.stride = len(array_data[0]) + + layout.dim.append(dim1) + layout.dim.append(dim2) + + msg.layout = layout + + publisher_.publish(msg) + + +# inherits from DataCollectingBaseNode +class DataCollectingDataCounter(DataCollectingBaseNode): + def __init__(self): + super().__init__("data_collecting_data_counter") + + self.vel_hist = deque([float(0.0)] * 200, maxlen=200) + self.acc_hist = deque([float(0.0)] * 200, maxlen=200) + + self.timer_period_callback = 0.033 # 30ms + self.timer_counter = self.create_timer( + self.timer_period_callback, + self.timer_callback_counter, + ) + + self.collected_data_counts_of_vel_acc_publisher_ = self.create_publisher( + Int32MultiArray, "/control_data_collecting_tools/collected_data_counts_of_vel_acc", 10 + ) + self.collected_data_counts_of_vel_steer_publisher_ = self.create_publisher( + Int32MultiArray, "/control_data_collecting_tools/collected_data_counts_of_vel_steer", 10 + ) + + self.vel_hist_publisher_ = self.create_publisher( + Float32MultiArray, "/control_data_collecting_tools/vel_hist", 10 + ) + self.acc_hist_publisher_ = self.create_publisher( + Float32MultiArray, "/control_data_collecting_tools/acc_hist", 10 + ) + + self.declare_parameter( + "LOAD_ROSBAG2_FILES", + True, + ParameterDescriptor( + description="Flag that determines whether to load rosbag2 data or not" + ), + ) + + load_rosbag2_files = ( + self.get_parameter("LOAD_ROSBAG2_FILES").get_parameter_value().bool_value + ) + + if load_rosbag2_files: + # candidates referencing the rosbag data + rosbag2_dir_list = [d for d in os.listdir("./") if os.path.isdir(os.path.join("./", d))] + # load rosbag data + self.load_rosbag_data(rosbag2_dir_list) + + def load_rosbag_data(self, rosbag2_dir_list): + for rosbag2_dir in rosbag2_dir_list: + # try to fetch /localization/acceleration and /localization/kinematic_state from rosbag2_file + rosbag2_file = "./" + rosbag2_dir + "/" + rosbag2_dir + "_0.db3" + db3reader = rosbag_play.db3Reader(rosbag2_file) + + load_acc_topic = db3reader.load_db3("/localization/acceleration") + load_kinematic_topic = db3reader.load_db3("/localization/kinematic_state") + + # if /localization/acceleration or /localization/kinematic_state is not include in the data base skip counting data points + if load_acc_topic and load_kinematic_topic: + acceleration, kinematic_state = db3reader.read_msg( + "/localization/acceleration" + ), db3reader.read_msg("/localization/kinematic_state") + if acceleration is None or kinematic_state is None: + continue + + previous_acc, previous_vel, previous_ang_vel_z = ( + acceleration.accel.accel.linear.x, + kinematic_state.twist.twist.linear.x, + kinematic_state.twist.twist.angular.z, + ) + previous_acc_time, previous_kinematic_time = ( + acceleration.header.stamp.sec + 1e-9 * acceleration.header.stamp.nanosec, + kinematic_state.header.stamp.sec + 1e-9 * kinematic_state.header.stamp.nanosec, + ) + current_time = max([previous_acc_time, previous_kinematic_time]) + + acceleration, kinematic_state = db3reader.read_msg( + "/localization/acceleration" + ), db3reader.read_msg("/localization/kinematic_state") + if acceleration is None or kinematic_state is None: + break + current_acc_time, current_kinematic_time = ( + acceleration.header.stamp.sec + 1e-9 * acceleration.header.stamp.nanosec, + kinematic_state.header.stamp.sec + 1e-9 * kinematic_state.header.stamp.nanosec, + ) + + # A while loop for counting data points + while True: + # interpolate acceleration if necessary + while current_time > current_acc_time: + previous_acc = acceleration.accel.accel.linear.x + acceleration = db3reader.read_msg("/localization/acceleration") + if acceleration is None: + break + previous_acc_time = current_acc_time + current_acc_time = ( + acceleration.header.stamp.sec + 1e-9 * acceleration.header.stamp.nanosec + ) + + if acceleration is None: + break + acc = (current_acc_time - current_time) * acceleration.accel.accel.linear.x + ( + current_time - previous_acc_time + ) * previous_acc + acc /= current_acc_time - previous_acc_time + + # interpolate kinematic state if necessary + while current_time > current_kinematic_time: + previous_vel, previous_ang_vel_z = ( + kinematic_state.twist.twist.linear.x, + kinematic_state.twist.twist.angular.z, + ) + kinematic_state = db3reader.read_msg("/localization/kinematic_state") + if kinematic_state is None: + break + previous_kinematic_time = current_kinematic_time + current_kinematic_time = ( + kinematic_state.header.stamp.sec + + 1e-9 * kinematic_state.header.stamp.nanosec + ) + + if kinematic_state is None: + break + vel = ( + current_kinematic_time - current_time + ) * kinematic_state.twist.twist.linear.x + ( + current_time - previous_kinematic_time + ) * previous_vel + vel /= current_kinematic_time - previous_kinematic_time + + ang_vel_z = ( + current_kinematic_time - current_time + ) * kinematic_state.twist.twist.angular.z + ( + current_time - previous_kinematic_time + ) * previous_ang_vel_z + ang_vel_z /= current_kinematic_time - previous_kinematic_time + + # calculation of steer + wheel_base = self.get_parameter("wheel_base").get_parameter_value().double_value + steer = arctan2(wheel_base * ang_vel_z, kinematic_state.twist.twist.linear.x) + + # count number of data + if kinematic_state.twist.twist.linear.x > 1e-3: + self.count_observations( + vel, + acc, + steer, + ) + + current_time += self.timer_period_callback + + def count_observations(self, v, a, steer): + v_bin = np.digitize(v, self.v_bins) - 1 + steer_bin = np.digitize(steer, self.steer_bins) - 1 + a_bin = np.digitize(a, self.a_bins) - 1 + + if 0 <= v_bin < self.num_bins_v and 0 <= a_bin < self.num_bins_a: + self.collected_data_counts_of_vel_acc[v_bin, a_bin] += 1 + + if 0 <= v_bin < self.num_bins_v and 0 <= steer_bin < self.num_bins_steer: + self.collected_data_counts_of_vel_steer[v_bin, steer_bin] += 1 + + # call back for counting data points + def timer_callback_counter(self): + if ( + self._present_kinematic_state is not None + and self._present_acceleration is not None + and self.present_operation_mode_ == 3 + and self._present_control_mode_ == 1 + ): + # calculate steer + angular_z = self._present_kinematic_state.twist.twist.angular.z + wheel_base = self.get_parameter("wheel_base").get_parameter_value().double_value + current_steer = arctan2( + wheel_base * angular_z, self._present_kinematic_state.twist.twist.linear.x + ) + + current_vel = self._present_kinematic_state.twist.twist.linear.x + current_acc = self._present_acceleration.accel.accel.linear.x + + if self._present_kinematic_state.twist.twist.linear.x > 1e-3: + self.count_observations( + current_vel, + current_acc, + current_steer, + ) + + self.acc_hist.append(float(current_acc)) + self.vel_hist.append(float(current_vel)) + + # publish collected_data_counts_of_vel_acc + publish_Int32MultiArray( + self.collected_data_counts_of_vel_acc_publisher_, self.collected_data_counts_of_vel_acc + ) + + # publish collected_data_counts_of_vel_steer + publish_Int32MultiArray( + self.collected_data_counts_of_vel_steer_publisher_, + self.collected_data_counts_of_vel_steer, + ) + + # publish acc_hist + msg = Float32MultiArray() + msg.data = list(self.acc_hist) + self.acc_hist_publisher_.publish(msg) + + # publish vel_hist + msg = Float32MultiArray() + msg.data = list(self.vel_hist) + self.vel_hist_publisher_.publish(msg) + + +def main(args=None): + rclpy.init(args=args) + + data_collecting_data_counter = DataCollectingDataCounter() + rclpy.spin(data_collecting_data_counter) + + data_collecting_data_counter.destroy_node() + rclpy.shutdown() + + +if __name__ == "__main__": + main() diff --git a/control_data_collecting_tool/scripts/data_collecting_plotter.py b/control_data_collecting_tool/scripts/data_collecting_plotter.py index 30f1f2cc..93c35276 100755 --- a/control_data_collecting_tool/scripts/data_collecting_plotter.py +++ b/control_data_collecting_tool/scripts/data_collecting_plotter.py @@ -14,238 +14,88 @@ # See the License for the specific language governing permissions and # limitations under the License. -import threading - -from geometry_msgs.msg import AccelWithCovarianceStamped +from data_collecting_base_node import DataCollectingBaseNode import matplotlib.pyplot as plt -from nav_msgs.msg import Odometry import numpy as np -from numpy import arctan2 -from rcl_interfaces.msg import ParameterDescriptor import rclpy -from rclpy.callback_groups import ReentrantCallbackGroup -from rclpy.node import Node import seaborn as sns +from std_msgs.msg import Float32MultiArray +from std_msgs.msg import Int32MultiArray -class DataCollectingPlotter(Node): +class DataCollectingPlotter(DataCollectingBaseNode): def __init__(self): super().__init__("data_collecting_plotter") - self.declare_parameter( - "NUM_BINS_V", - 10, - ParameterDescriptor(description="Number of bins of velocity in heatmap"), - ) - - self.declare_parameter( - "NUM_BINS_STEER", - 10, - ParameterDescriptor(description="Number of bins of steer in heatmap"), - ) - - self.declare_parameter( - "NUM_BINS_A", - 10, - ParameterDescriptor(description="Number of bins of acceleration in heatmap"), - ) - - self.declare_parameter( - "V_MIN", - 0.0, - ParameterDescriptor(description="Maximum velocity in heatmap [m/s]"), - ) - - self.declare_parameter( - "V_MAX", - 1.0, - ParameterDescriptor(description="Minimum steer in heatmap [m/s]"), - ) - - self.declare_parameter( - "STEER_MIN", - -1.0, - ParameterDescriptor(description="Maximum steer in heatmap [rad]"), - ) - - self.declare_parameter( - "STEER_MAX", - 1.0, - ParameterDescriptor(description="Maximum steer in heatmap [rad]"), - ) - - self.declare_parameter( - "A_MIN", - -1.0, - ParameterDescriptor(description="Minimum acceleration in heatmap [m/ss]"), - ) - - self.declare_parameter( - "A_MAX", - 1.0, - ParameterDescriptor(description="Maximum acceleration in heatmap [m/ss]"), - ) - - self.declare_parameter( - "wheel_base", - 2.79, # sample_vehicle_launch/sample_vehicle_description/config/vehicle_info.param.yaml - ParameterDescriptor(description="Wheel base [m]"), - ) - - self.sub_odometry_ = self.create_subscription( - Odometry, - "/localization/kinematic_state", - self.onOdometry, - 1, - ) - - self.sub_acceleration_ = self.create_subscription( - AccelWithCovarianceStamped, - "/localization/acceleration", - self.onAcceleration, - 1, - ) - - self._present_kinematic_state = None - self._present_acceleration = None - - # velocity and acceleration grid - # velocity and steer grid - self.num_bins_v = self.get_parameter("NUM_BINS_V").get_parameter_value().integer_value - self.num_bins_steer = ( - self.get_parameter("NUM_BINS_STEER").get_parameter_value().integer_value - ) - self.num_bins_a = self.get_parameter("NUM_BINS_A").get_parameter_value().integer_value - self.v_min, self.v_max = ( - self.get_parameter("V_MIN").get_parameter_value().double_value, - self.get_parameter("V_MAX").get_parameter_value().double_value, - ) - self.steer_min, self.steer_max = ( - self.get_parameter("STEER_MIN").get_parameter_value().double_value, - self.get_parameter("STEER_MAX").get_parameter_value().double_value, - ) - self.a_min, self.a_max = ( - self.get_parameter("A_MIN").get_parameter_value().double_value, - self.get_parameter("A_MAX").get_parameter_value().double_value, - ) - - self.collected_data_counts_of_vel_acc = np.zeros((self.num_bins_v, self.num_bins_a)) - self.collected_data_counts_of_vel_steer = np.zeros((self.num_bins_v, self.num_bins_steer)) - - self.v_bins = np.linspace(self.v_min, self.v_max, self.num_bins_v + 1) - self.steer_bins = np.linspace(self.steer_min, self.steer_max, self.num_bins_steer + 1) - self.a_bins = np.linspace(self.a_min, self.a_max, self.num_bins_a + 1) - - self.v_bin_centers = (self.v_bins[:-1] + self.v_bins[1:]) / 2 - self.steer_bin_centers = (self.steer_bins[:-1] + self.steer_bins[1:]) / 2 - self.a_bin_centers = (self.a_bins[:-1] + self.a_bins[1:]) / 2 - - self.vel_hist = np.zeros(200) - self.acc_hist = np.zeros(200) - - self.vel_hist_for_plot = np.zeros(200) - self.acc_hist_for_plot = np.zeros(200) - - # create callback groups - self.callback_group = ReentrantCallbackGroup() - self.lock = threading.Lock() - - # callback for data count - self.timer_period_callback = 0.033 # 30ms - self.timer_counter = self.create_timer( - self.timer_period_callback, - self.timer_callback_counter, - callback_group=self.callback_group, - ) - # callback for plot self.grid_update_time_interval = 5.0 self.timer_plotter = self.create_timer( self.grid_update_time_interval, self.timer_callback_plotter, - callback_group=self.callback_group, ) self.fig, self.axs = plt.subplots(3, 1, figsize=(12, 20)) plt.ion() - self.collected_data_counts_of_vel_acc_for_plot = np.zeros( - (self.num_bins_v, self.num_bins_a) - ) - self.collected_data_counts_of_vel_steer_for_plot = np.zeros( - (self.num_bins_v, self.num_bins_steer) + self.collected_data_counts_of_vel_acc_subscription_ = self.create_subscription( + Int32MultiArray, + "/control_data_collecting_tools/collected_data_counts_of_vel_acc", + self.subscribe_collected_data_counts_of_vel_acc, + 10, ) + self.collected_data_counts_of_vel_acc_subscription_ - self.v_bin_centers_for_plot = (self.v_bins[:-1] + self.v_bins[1:]) / 2 - self.steer_bin_centers_for_plot = (self.steer_bins[:-1] + self.steer_bins[1:]) / 2 - self.a_bin_centers_for_plot = (self.a_bins[:-1] + self.a_bins[1:]) / 2 - - def onOdometry(self, msg): - self._present_kinematic_state = msg + self.collected_data_counts_of_vel_steer_subscription_ = self.create_subscription( + Int32MultiArray, + "/control_data_collecting_tools/collected_data_counts_of_vel_steer", + self.subscribe_collected_data_counts_of_vel_steer, + 10, + ) + self.collected_data_counts_of_vel_steer_subscription_ - def onAcceleration(self, msg): - self._present_acceleration = msg + self.acc_hist_subscription_ = self.create_subscription( + Float32MultiArray, + "/control_data_collecting_tools/acc_hist", + self.subscribe_acc_hist, + 10, + ) + self.acc_hist_subscription_ - def count_observations(self, v, a, steer): - v_bin = np.digitize(v, self.v_bins) - 1 - steer_bin = np.digitize(steer, self.steer_bins) - 1 - a_bin = np.digitize(a, self.a_bins) - 1 + self.vel_hist_subscription_ = self.create_subscription( + Float32MultiArray, + "/control_data_collecting_tools/vel_hist", + self.subscribe_vel_hist, + 10, + ) + self.vel_hist_subscription_ - if 0 <= v_bin < self.num_bins_v and 0 <= a_bin < self.num_bins_a: - self.collected_data_counts_of_vel_acc[v_bin, a_bin] += 1 + self.acc_hist = [0.0] * 200 + self.vel_hist = [0.0] * 200 - if 0 <= v_bin < self.num_bins_v and 0 <= steer_bin < self.num_bins_steer: - self.collected_data_counts_of_vel_steer[v_bin, steer_bin] += 1 + def subscribe_collected_data_counts_of_vel_acc(self, msg): + rows = msg.layout.dim[0].size + cols = msg.layout.dim[1].size + self.collected_data_counts_of_vel_acc = np.array(msg.data).reshape((rows, cols)) - def timer_callback_plotter(self): - with self.lock: - self.collected_data_counts_of_vel_acc_for_plot = ( - self.collected_data_counts_of_vel_acc.copy() - ) - self.collected_data_counts_of_vel_steer_for_plot = ( - self.collected_data_counts_of_vel_steer.copy() - ) + def subscribe_collected_data_counts_of_vel_steer(self, msg): + rows = msg.layout.dim[0].size + cols = msg.layout.dim[1].size + self.collected_data_counts_of_vel_steer = np.array(msg.data).reshape((rows, cols)) - self.acc_hist_for_plot = self.acc_hist.copy() - self.vel_hist_for_plot = self.vel_hist.copy() + def subscribe_acc_hist(self, msg): + self.acc_hist = msg.data - self.v_bin_centers_for_plot = self.v_bin_centers - self.steer_bin_centers_for_plot = self.steer_bin_centers - self.a_bin_centers_for_plot = self.a_bin_centers + def subscribe_vel_hist(self, msg): + self.vel_hist = msg.data + def timer_callback_plotter(self): self.plot_data_collection_grid() - plt.pause(0.01) - - def timer_callback_counter(self): - if self._present_kinematic_state is not None and self._present_acceleration is not None: - # calculate steer - angular_z = self._present_kinematic_state.twist.twist.angular.z - wheel_base = self.get_parameter("wheel_base").get_parameter_value().double_value - current_steer = arctan2( - wheel_base * angular_z, self._present_kinematic_state.twist.twist.linear.x - ) - - current_vel = self._present_kinematic_state.twist.twist.linear.x - current_acc = self._present_acceleration.accel.accel.linear.x - - # update velocity and acceleration bin if ego vehicle is moving - if self._present_kinematic_state.twist.twist.linear.x > 1e-3: - with self.lock: - self.count_observations( - current_vel, - current_acc, - current_steer, - ) - - self.acc_hist[:-1] = 1.0 * self.acc_hist[1:] - self.acc_hist[-1] = current_acc - self.vel_hist[:-1] = 1.0 * self.vel_hist[1:] - self.vel_hist[-1] = current_vel + plt.pause(0.1) def plot_data_collection_grid(self): self.axs[0].cla() - self.axs[0].scatter(self.acc_hist_for_plot, self.vel_hist_for_plot) - self.axs[0].plot(self.acc_hist_for_plot, self.vel_hist_for_plot) + self.axs[0].scatter(self.acc_hist, self.vel_hist) + self.axs[0].plot(self.acc_hist, self.vel_hist) self.axs[0].set_xlim([-2.0, 2.0]) self.axs[0].set_ylim([0.0, self.v_max + 1.0]) self.axs[0].set_xlabel("Acceleration") @@ -258,11 +108,11 @@ def plot_data_collection_grid(self): self.axs[1].cla() self.heatmap = sns.heatmap( - self.collected_data_counts_of_vel_acc_for_plot.T, + self.collected_data_counts_of_vel_acc.T, annot=True, cmap="coolwarm", - xticklabels=np.round(self.v_bin_centers_for_plot, 2), - yticklabels=np.round(self.a_bin_centers_for_plot, 2), + xticklabels=np.round(self.v_bin_centers, 2), + yticklabels=np.round(self.a_bin_centers, 2), ax=self.axs[1], linewidths=0.1, linecolor="gray", @@ -277,11 +127,11 @@ def plot_data_collection_grid(self): self.axs[2].cla() self.heatmap = sns.heatmap( - self.collected_data_counts_of_vel_steer_for_plot.T, + self.collected_data_counts_of_vel_steer.T, annot=True, cmap="coolwarm", - xticklabels=np.round(self.v_bin_centers_for_plot, 2), - yticklabels=np.round(self.steer_bin_centers_for_plot, 2), + xticklabels=np.round(self.v_bin_centers, 2), + yticklabels=np.round(self.steer_bin_centers, 2), ax=self.axs[2], linewidths=0.1, linecolor="gray", @@ -293,8 +143,6 @@ def plot_data_collection_grid(self): self.fig.canvas.draw() - plt.pause(0.01) - def main(args=None): rclpy.init(args=args) diff --git a/control_data_collecting_tool/scripts/data_collecting_rosbag_record.py b/control_data_collecting_tool/scripts/data_collecting_rosbag_record.py new file mode 100755 index 00000000..ac29dbc4 --- /dev/null +++ b/control_data_collecting_tool/scripts/data_collecting_rosbag_record.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 + +# Copyright 2024 Proxima Technology Inc, TIER IV +# +# 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 datetime import datetime +from functools import partial +import os + +from ament_index_python.packages import get_package_share_directory +from autoware_adapi_v1_msgs.msg import OperationModeState +from autoware_vehicle_msgs.msg import ControlModeReport +import rclpy +from rclpy.node import Node +from rclpy.serialization import serialize_message +from rosbag2_py import ConverterOptions +from rosbag2_py import SequentialWriter +from rosbag2_py import StorageOptions +from rosbag2_py._storage import TopicMetadata +from rosidl_runtime_py.utilities import get_message +import yaml + + +class MessageWriter: + def __init__(self, topics, node): + self.topics = topics + self.node = node + self.message_writer = None + self.message_subscriptions_ = [] + + def create_writer(self): + self.message_writer = SequentialWriter() + + def subscribe_topics(self): + topic_type_list = [] + unsubscribed_topic = [] + + # Get topic types from the list of topics and store them + for topic_name in self.topics: + topic_type = self.get_topic_type(topic_name) + topic_type_list.append(topic_type) + if topic_type is None: + # If a topic type is not found, mark it as unsubscribed + unsubscribed_topic.append(topic_name) + + # If some topics are not found, log a message and skip the subscription + if len(unsubscribed_topic) > 0: + self.node.get_logger().info(f"Failed to get type for topic: {unsubscribed_topic}") + return + + # Generate a unique directory and filename based on the current time for the rosbag file + now = datetime.now() + formatted_time = now.strftime("%Y_%m_%d-%H_%M_%S") + bag_dir = f"./rosbag2_{formatted_time}" + storage_options = StorageOptions(uri=bag_dir, storage_id="sqlite3") + converter_options = ConverterOptions( + input_serialization_format="cdr", output_serialization_format="cdr" + ) + + # Open the rosbag for writing + try: + self.message_writer.open(storage_options, converter_options) + except Exception as e: + self.node.get_logger().error(f"Failed to open bag: {e}") + return + + # Create topics in the rosbag for recording + for topic_name, topic_type in zip(self.topics, topic_type_list): + self.node.get_logger().info(f"Recording topic: {topic_name} of type: {topic_type}") + topic_metadata = TopicMetadata( + name=topic_name, type=topic_type, serialization_format="cdr" + ) + self.message_writer.create_topic(topic_metadata) + + def get_topic_type(self, topic_name): + # Get the list of topics and their types from the ROS node + topic_names_and_types = self.node.get_topic_names_and_types() + for name, types in topic_names_and_types: + if name == topic_name: + return types[0] + return None + + def start_record(self): + # Subscribe to the topics and start recording messages + for topic_name in self.topics: + topic_type = self.get_topic_type(topic_name) + if topic_type: + msg_module = get_message(topic_type) + subscription_ = self.node.create_subscription( + msg_module, topic_name, partial(self.callback_write_message, topic_name), 10 + ) + self.message_subscriptions_.append(subscription_) + self.node.get_logger().info("start recording rosbag") + + # call back function called in start recording + def callback_write_message(self, topic_name, message): + try: + serialized_message = serialize_message(message) + current_time = rclpy.clock.Clock().now() + self.message_writer.write(topic_name, serialized_message, current_time.nanoseconds) + except Exception as e: + self.node.get_logger().error(f"Failed to write message: {e}") + + def stop_record(self): + # Stop recording by destroying the subscriptions and deleting the writer + for subscription_ in self.message_subscriptions_: + self.node.destroy_subscription(subscription_) + del self.message_writer + self.node.get_logger().info("stop recording rosbag") + + +class DataCollectingRosbagRecord(Node): + def __init__(self): + super().__init__("data_collecting_rosbag_record") + package_share_directory = get_package_share_directory("control_data_collecting_tool") + topic_file_path = os.path.join(package_share_directory, "config", "topics.yaml") + with open(topic_file_path, "r") as file: + topic_data = yaml.safe_load(file) + self.topics = topic_data["topics"] + self.writer = MessageWriter(self.topics, self) + self.subscribed = False + self.recording = False + + self.present_operation_mode_ = None + self.operation_mode_subscription_ = self.create_subscription( + OperationModeState, + "/system/operation_mode/state", + self.subscribe_operation_mode, + 10, + ) + + self._present_control_mode_ = None + self.control_mode_subscription_ = self.create_subscription( + ControlModeReport, + "/vehicle/status/control_mode", + self.subscribe_control_mode, + 10, + ) + + self.timer_period_callback = 1.0 + self.timer_callback = self.create_timer( + self.timer_period_callback, + self.record_message, + ) + + def subscribe_operation_mode(self, msg): + self.present_operation_mode_ = msg.mode + + def subscribe_control_mode(self, msg): + self._present_control_mode_ = msg.mode + + def record_message(self): + # Start subscribing to topics and recording if the operation mode is 3(LOCAL) and control mode is 1(AUTONOMOUS) + if ( + self.present_operation_mode_ == 3 + and self._present_control_mode_ == 1 + and not self.subscribed + and not self.recording + ): + self.writer.create_writer() + self.writer.subscribe_topics() + self.subscribed = True + + # Start recording if topics are subscribed and the operation mode is 3(LOCAL) + if ( + self.present_operation_mode_ == 3 + and self._present_control_mode_ == 1 + and self.subscribed + and not self.recording + ): + self.writer.start_record() + self.recording = True + + # Stop recording if the operation mode changes from 3(LOCAL) + if ( + (self.present_operation_mode_ != 3 or self._present_control_mode_ != 1) + and self.subscribed + and self.recording + ): + self.writer.stop_record() + self.subscribed = False + self.recording = False + + +def main(args=None): + rclpy.init(args=args) + + data_collecting_rosbag_record = DataCollectingRosbagRecord() + rclpy.spin(data_collecting_rosbag_record) + data_collecting_rosbag_record.destroy_node() + + rclpy.shutdown() + + +if __name__ == "__main__": + main() diff --git a/control_data_collecting_tool/scripts/data_collecting_trajectory_publisher.py b/control_data_collecting_tool/scripts/data_collecting_trajectory_publisher.py index d8c396ac..cbc86d47 100755 --- a/control_data_collecting_tool/scripts/data_collecting_trajectory_publisher.py +++ b/control_data_collecting_tool/scripts/data_collecting_trajectory_publisher.py @@ -16,21 +16,19 @@ from autoware_planning_msgs.msg import Trajectory from autoware_planning_msgs.msg import TrajectoryPoint -from geometry_msgs.msg import AccelWithCovarianceStamped +from data_collecting_base_node import DataCollectingBaseNode from geometry_msgs.msg import Point from geometry_msgs.msg import PolygonStamped import matplotlib.pyplot as plt -from nav_msgs.msg import Odometry import numpy as np -from numpy import arctan2 from numpy import cos from numpy import pi from numpy import sin from rcl_interfaces.msg import ParameterDescriptor import rclpy -from rclpy.node import Node from scipy.spatial.transform import Rotation as R -import seaborn as sns +from std_msgs.msg import Bool +from std_msgs.msg import Int32MultiArray from visualization_msgs.msg import Marker from visualization_msgs.msg import MarkerArray @@ -54,6 +52,10 @@ def getYaw(orientation_xyzw): return R.from_quat(orientation_xyzw.reshape(-1, 4)).as_euler("xyz")[:, 2] +def computeTriangleArea(A, B, C): + return 0.5 * abs(np.cross(B - A, C - A)) + + def get_eight_course_trajectory_points( long_side_length: float, short_side_length: float, @@ -96,7 +98,6 @@ def get_eight_course_trajectory_points( break if 0 <= t and t <= OB: - print(t) x[i] = (b / 2 - (1.0 - np.sqrt(3) / 2) * a) * t / OB y[i] = a * t / (2 * OB) yaw[i] = θB @@ -120,7 +121,6 @@ def get_eight_course_trajectory_points( y[i] = D[1] + a * t2 / (2 * OB) yaw[i] = np.pi - θB curve[i] = 1e-10 - parts[i] = "linear_negative" achievement_rates[i] = t2 / (2 * OB) @@ -131,7 +131,6 @@ def get_eight_course_trajectory_points( y[i] = OL[1] - R * np.sin(-np.pi / 6 + t3_rad) yaw[i] = np.pi + t3_rad curve[i] = 1 / R - parts[i] = "left_circle" achievement_rates[i] = t3 / BD @@ -141,7 +140,6 @@ def get_eight_course_trajectory_points( y[i] = C[1] + a * t4 / (2 * OB) yaw[i] = θB curve[i] = 1e-10 - parts[i] = "linear_positive" achievement_rates[i] = t4 / (2 * OB) @@ -275,7 +273,8 @@ def get_u_shaped_return_course_trajectory_points( return np.array([x, y]).T, yaw, curve[:i_end], parts, achievement_rates -class DataCollectingTrajectoryPublisher(Node): +# inherits from DataCollectingBaseNode +class DataCollectingTrajectoryPublisher(DataCollectingBaseNode): def __init__(self): super().__init__("data_collecting_trajectory_publisher") @@ -287,66 +286,6 @@ def __init__(self): ), ) - self.declare_parameter( - "NUM_BINS_V", - 10, - ParameterDescriptor(description="Number of bins of velocity in heatmap"), - ) - - self.declare_parameter( - "NUM_BINS_STEER", - 10, - ParameterDescriptor(description="Number of bins of steer in heatmap"), - ) - - self.declare_parameter( - "NUM_BINS_A", - 10, - ParameterDescriptor(description="Number of bins of acceleration in heatmap"), - ) - - self.declare_parameter( - "V_MIN", - -1.0, - ParameterDescriptor(description="Maximum velocity in heatmap [m/s]"), - ) - - self.declare_parameter( - "V_MAX", - 1.0, - ParameterDescriptor(description="Minimum steer in heatmap [m/s]"), - ) - - self.declare_parameter( - "STEER_MIN", - -1.0, - ParameterDescriptor(description="Maximum steer in heatmap [rad]"), - ) - - self.declare_parameter( - "STEER_MAX", - 1.0, - ParameterDescriptor(description="Maximum steer in heatmap [rad]"), - ) - - self.declare_parameter( - "A_MIN", - -1.0, - ParameterDescriptor(description="Minimum acceleration in heatmap [m/ss]"), - ) - - self.declare_parameter( - "A_MAX", - 1.0, - ParameterDescriptor(description="Maximum acceleration in heatmap [m/ss]"), - ) - - self.declare_parameter( - "wheel_base", - 2.79, # sample_vehicle_launch/sample_vehicle_description/config/vehicle_info.param.yaml - ParameterDescriptor(description="Wheel base [m]"), - ) - self.declare_parameter( "acc_kp", 1.0, @@ -385,7 +324,7 @@ def __init__(self): self.declare_parameter( "mov_ave_window", - 100, + 50, ParameterDescriptor(description="Moving average smoothing window size"), ) @@ -419,6 +358,30 @@ def __init__(self): ), ) + self.declare_parameter( + "COLLECTING_DATA_V_MIN", + 0.0, + ParameterDescriptor(description="Minimum velocity for data collection [m/s]"), + ) + + self.declare_parameter( + "COLLECTING_DATA_V_MAX", + 11.5, + ParameterDescriptor(description="Maximum velocity for data collection [m/s]"), + ) + + self.declare_parameter( + "COLLECTING_DATA_A_MIN", + -1.0, + ParameterDescriptor(description="Minimum velocity for data collection [m/ss]"), + ) + + self.declare_parameter( + "COLLECTING_DATA_A_MAX", + 1.0, + ParameterDescriptor(description="Maximum velocity for data collection [m/ss]"), + ) + self.trajectory_for_collecting_data_pub_ = self.create_publisher( Trajectory, "/data_collecting_trajectory", @@ -431,17 +394,9 @@ def __init__(self): 1, ) - self.sub_odometry_ = self.create_subscription( - Odometry, - "/localization/kinematic_state", - self.onOdometry, - 1, - ) - - self.sub_acceleration_ = self.create_subscription( - AccelWithCovarianceStamped, - "/localization/acceleration", - self.onAcceleration, + self.pub_stop_request_ = self.create_publisher( + Bool, + "/data_collecting_stop_request", 1, ) @@ -456,37 +411,6 @@ def __init__(self): # set course name self.COURSE_NAME = self.get_parameter("COURSE_NAME").value - # velocity and acceleration grid - # velocity and steer grid - self.num_bins_v = self.get_parameter("NUM_BINS_V").get_parameter_value().integer_value - self.num_bins_steer = ( - self.get_parameter("NUM_BINS_STEER").get_parameter_value().integer_value - ) - self.num_bins_a = self.get_parameter("NUM_BINS_A").get_parameter_value().integer_value - self.v_min, self.v_max = ( - self.get_parameter("V_MIN").get_parameter_value().double_value, - self.get_parameter("V_MAX").get_parameter_value().double_value, - ) - self.steer_min, self.steer_max = ( - self.get_parameter("STEER_MIN").get_parameter_value().double_value, - self.get_parameter("STEER_MAX").get_parameter_value().double_value, - ) - self.a_min, self.a_max = ( - self.get_parameter("A_MIN").get_parameter_value().double_value, - self.get_parameter("A_MAX").get_parameter_value().double_value, - ) - - self.collected_data_counts_of_vel_acc = np.zeros((self.num_bins_v, self.num_bins_a)) - self.collected_data_counts_of_vel_steer = np.zeros((self.num_bins_v, self.num_bins_steer)) - - self.v_bins = np.linspace(self.v_min, self.v_max, self.num_bins_v + 1) - self.steer_bins = np.linspace(self.steer_min, self.steer_max, self.num_bins_steer + 1) - self.a_bins = np.linspace(self.a_min, self.a_max, self.num_bins_a + 1) - - self.v_bin_centers = (self.v_bins[:-1] + self.v_bins[1:]) / 2 - self.steer_bin_centers = (self.steer_bins[:-1] + self.steer_bins[1:]) / 2 - self.a_bin_centers = (self.a_bins[:-1] + self.a_bins[1:]) / 2 - self.trajectory_parts = None self.trajectory_achievement_rates = None @@ -501,9 +425,6 @@ def __init__(self): self.on_line_vel_flag = True self.deceleration_rate = 0.6 - self.vel_hist = np.zeros(200) - self.acc_hist = np.zeros(200) - self.timer_period_callback = 0.03 # 30ms self.traj_step = 0.1 self.timer_traj = self.create_timer(self.timer_period_callback, self.timer_callback_traj) @@ -527,14 +448,53 @@ def __init__(self): ) self.one_round_progress_rate = None - self.vel_noise_list = [] - def onOdometry(self, msg): - self._present_kinematic_state = msg + collecting_data_min_v, collecting_data_max_v = ( + self.get_parameter("COLLECTING_DATA_V_MIN").get_parameter_value().double_value, + self.get_parameter("COLLECTING_DATA_V_MAX").get_parameter_value().double_value, + ) + + collecting_data_min_a, collecting_data_max_a = ( + self.get_parameter("COLLECTING_DATA_A_MIN").get_parameter_value().double_value, + self.get_parameter("COLLECTING_DATA_A_MAX").get_parameter_value().double_value, + ) + + self.collecting_data_min_n_v = max([np.digitize(collecting_data_min_v, self.v_bins) - 1, 0]) + self.collecting_data_max_n_v = ( + min([np.digitize(collecting_data_max_v, self.v_bins) - 1, self.num_bins_v - 1]) + 1 + ) + + self.collecting_data_min_n_a = max([np.digitize(collecting_data_min_a, self.v_bins) - 1, 0]) + self.collecting_data_max_n_a = ( + min([np.digitize(collecting_data_max_a, self.v_bins) - 1, self.num_bins_a - 1]) + 1 + ) + + self.collected_data_counts_of_vel_acc_subscription_ = self.create_subscription( + Int32MultiArray, + "/control_data_collecting_tools/collected_data_counts_of_vel_acc", + self.subscribe_collected_data_counts_of_vel_acc, + 10, + ) + self.collected_data_counts_of_vel_acc_subscription_ + + self.collected_data_counts_of_vel_steer_subscription_ = self.create_subscription( + Int32MultiArray, + "/control_data_collecting_tools/collected_data_counts_of_vel_steer", + self.subscribe_collected_data_counts_of_vel_steer, + 10, + ) + self.collected_data_counts_of_vel_steer_subscription_ + + def subscribe_collected_data_counts_of_vel_acc(self, msg): + rows = msg.layout.dim[0].size + cols = msg.layout.dim[1].size + self.collected_data_counts_of_vel_acc = np.array(msg.data).reshape((rows, cols)) - def onAcceleration(self, msg): - self._present_acceleration = msg + def subscribe_collected_data_counts_of_vel_steer(self, msg): + rows = msg.layout.dim[0].size + cols = msg.layout.dim[1].size + self.collected_data_counts_of_vel_steer = np.array(msg.data).reshape((rows, cols)) def onDataCollectingArea(self, msg): self._data_collecting_area_polygon = msg @@ -546,17 +506,11 @@ def get_target_velocity(self, nearestIndex): ] # "left_circle", "right_circle", "linear_positive", "linear_negative" achievement_rate = self.trajectory_achievement_rates[nearestIndex] current_vel = self._present_kinematic_state.twist.twist.linear.x - current_acc = self._present_acceleration.accel.accel.linear.x acc_kp_of_pure_pursuit = self.get_parameter("acc_kp").get_parameter_value().double_value N_V = self.num_bins_v N_A = self.num_bins_a - self.acc_hist[:-1] = 1.0 * self.acc_hist[1:] - self.acc_hist[-1] = current_acc - self.vel_hist[:-1] = 1.0 * self.vel_hist[1:] - self.vel_hist[-1] = current_vel - max_lateral_accel = ( self.get_parameter("max_lateral_accel").get_parameter_value().double_value ) @@ -596,8 +550,8 @@ def get_target_velocity(self, nearestIndex): (-1 + N_V, -3 + N_A), ] - for i in range(0, N_V): - for j in range(0, N_A): + for i in range(self.collecting_data_min_n_v, self.collecting_data_max_n_v): + for j in range(self.collecting_data_min_n_a, self.collecting_data_max_n_a): if (i, j) not in exclude_idx_list: if ( min_num_data - min_data_num_margin @@ -623,11 +577,11 @@ def get_target_velocity(self, nearestIndex): or self.COURSE_NAME == "straight_line_negative" ): if self.target_vel_on_line > self.v_max * 3.0 / 4.0: - self.deceleration_rate = 0.55 + 0.15 + self.deceleration_rate = 0.55 + 0.10 elif self.target_vel_on_line > self.v_max / 2.0: - self.deceleration_rate = 0.65 + 0.25 + self.deceleration_rate = 0.65 + 0.10 else: - self.deceleration_rate = 0.85 + 0.1 + self.deceleration_rate = 0.85 + 0.10 elif self.COURSE_NAME == "eight_course": if self.target_vel_on_line > self.v_max * 3.0 / 4.0: @@ -915,64 +869,39 @@ def updateNominalTargetTrajectory(self): self.get_logger().info("update nominal target trajectory") - def count_observations(self, v, a, steer): - v_bin = np.digitize(v, self.v_bins) - 1 - steer_bin = np.digitize(steer, self.steer_bins) - 1 - a_bin = np.digitize(a, self.a_bins) - 1 - - if 0 <= v_bin < self.num_bins_v and 0 <= a_bin < self.num_bins_a: - self.collected_data_counts_of_vel_acc[v_bin, a_bin] += 1 - - if 0 <= v_bin < self.num_bins_v and 0 <= steer_bin < self.num_bins_steer: - self.collected_data_counts_of_vel_steer[v_bin, steer_bin] += 1 - - def plot_data_collection_grid(self): - self.axs[1].cla() - self.axs[1].scatter(self.acc_hist, self.vel_hist) - self.axs[1].plot(self.acc_hist, self.vel_hist) - self.axs[1].set_xlim([-2.0, 2.0]) - self.axs[1].set_ylim([0.0, self.v_max + 1.0]) - self.axs[1].set_xlabel("Acceleration") - self.axs[1].set_ylabel("Velocity") - - # update collected acceleration and velocity grid - for collection in self.axs[2].collections: - if collection.colorbar is not None: - collection.colorbar.remove() - self.axs[2].cla() - - self.heatmap = sns.heatmap( - self.collected_data_counts_of_vel_acc.T, - annot=True, - cmap="coolwarm", - xticklabels=np.round(self.v_bin_centers, 2), - yticklabels=np.round(self.a_bin_centers, 2), - ax=self.axs[2], - linewidths=0.1, - linecolor="gray", + def checkInDateCollectingArea(self, current_pos): + data_collecting_area = np.array( + [ + np.array( + [ + self._data_collecting_area_polygon.polygon.points[i].x, + self._data_collecting_area_polygon.polygon.points[i].y, + self._data_collecting_area_polygon.polygon.points[i].z, + ] + ) + for i in range(4) + ] ) - self.axs[2].set_xlabel("Velocity bins") - self.axs[2].set_ylabel("Acceleration bins") - - for collection in self.axs[3].collections: - if collection.colorbar is not None: - collection.colorbar.remove() - self.axs[3].cla() - - self.heatmap = sns.heatmap( - self.collected_data_counts_of_vel_steer.T, - annot=True, - cmap="coolwarm", - xticklabels=np.round(self.v_bin_centers, 2), - yticklabels=np.round(self.steer_bin_centers, 2), - ax=self.axs[3], - linewidths=0.1, - linecolor="gray", + A, B, C, D = ( + data_collecting_area[0][0:2], + data_collecting_area[1][0:2], + data_collecting_area[2][0:2], + data_collecting_area[3][0:2], ) + P = current_pos[0:2] - self.axs[3].set_xlabel("Velocity bins") - self.axs[3].set_ylabel("Steer bins") + area_ABCD = computeTriangleArea(A, B, C) + computeTriangleArea(C, D, A) + + area_PAB = computeTriangleArea(P, A, B) + area_PBC = computeTriangleArea(P, B, C) + area_PCD = computeTriangleArea(P, C, D) + area_PDA = computeTriangleArea(P, D, A) + + if area_PAB + area_PBC + area_PCD + area_PDA > area_ABCD * 1.001: + return False + else: + return True def timer_callback_traj(self): if ( @@ -980,21 +909,6 @@ def timer_callback_traj(self): and self._present_acceleration is not None and self.trajectory_position_data is not None ): - # calculate steer - angular_z = self._present_kinematic_state.twist.twist.angular.z - wheel_base = self.get_parameter("wheel_base").get_parameter_value().double_value - steer = arctan2( - wheel_base * angular_z, self._present_kinematic_state.twist.twist.linear.x - ) - - # update velocity and acceleration bin if ego vehicle is moving - if self._present_kinematic_state.twist.twist.linear.x > 1e-3: - self.count_observations( - self._present_kinematic_state.twist.twist.linear.x, - self._present_acceleration.accel.accel.linear.x, - steer, - ) - # [0] update nominal target trajectory if changing related ros2 params target_longitudinal_velocity = ( self.get_parameter("target_longitudinal_velocity") @@ -1009,7 +923,7 @@ def timer_callback_traj(self): > 1e-6 or window != self.current_window ): - True # self.updateNominalTargetTrajectory() + self.updateNominalTargetTrajectory() # [1] receive observation from topic present_position = np.array( @@ -1034,7 +948,12 @@ def timer_callback_traj(self): self._present_kinematic_state.twist.twist.linear.z, ] ) - present_yaw = getYaw(present_orientation) + + if np.linalg.norm(present_orientation) < 1e-6: + present_yaw = self.previous_yaw + else: + present_yaw = getYaw(present_orientation) + self.previous_yaw = present_yaw # [2] get whole trajectory data trajectory_position_data = self.trajectory_position_data.copy() @@ -1332,6 +1251,11 @@ def timer_callback_traj(self): marker_array.markers.append(marker_arrow) self.data_collecting_trajectory_marker_array_pub_.publish(marker_array) + # [6-3] stop request + if not self.checkInDateCollectingArea(present_position): + msg = Bool() + msg.data = True + self.pub_stop_request_.publish(msg) if debug_matplotlib_plot_flag: self.axs[0].cla() @@ -1388,8 +1312,6 @@ def timer_callback_traj(self): self.axs[0].set_ylabel("longitudinal_velocity [m/s]") self.axs[0].legend(fontsize=8) - self.plot_data_collection_grid() - self.fig.canvas.draw() plt.pause(0.01) diff --git a/control_data_collecting_tool/scripts/rosbag_play.py b/control_data_collecting_tool/scripts/rosbag_play.py new file mode 100644 index 00000000..7ed4ed3f --- /dev/null +++ b/control_data_collecting_tool/scripts/rosbag_play.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 + +# Copyright 2024 Proxima Technology Inc, TIER IV +# +# 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. + +import sqlite3 + +from rclpy.serialization import deserialize_message +from rosidl_runtime_py.utilities import get_message + + +class db3SingleTopic: + def __init__(self, db3_file, topic_id): + self.conn = sqlite3.connect(db3_file) + self.c = self.conn.cursor() + self.c.execute("SELECT * from messages WHERE topic_id = ?", (topic_id,)) + self.is_open = True + + def __del__(self): + self.conn.close() + + # Fetch the next row from the SQLite query result + def fetch_row(self): + return self.c.fetchone() + + +class db3Reader: + def __init__(self, db3_file): + self.db3_file = db3_file + + self.db3_dict = {} + + def __del__(self): + # Clean up by deleting the topic-specific objects and closing any open database connections + for topic_db3 in self.db3_dict.values(): + del topic_db3["topic_db3"] + + # Load topic information from the .db3 file + def load_db3(self, target_topic_name): + # Try to establish a connection to the .db3 SQLite database and retrieve topics + try: + conn = sqlite3.connect(self.db3_file) + c = conn.cursor() + c.execute("SELECT * from({})".format("topics")) + topics = c.fetchall() + + except sqlite3.Error: + return False + + # Create a dictionary that maps topic names to their respective ID and message type + topic_types = { + topics[i][1]: {"topic_id": i + 1, "topic_type": get_message(topics[i][2])} + for i in range(len(topics)) + } + + # Check if the target topic is included in the database + target_topic_included = True + if target_topic_name not in list(topic_types.keys()): + target_topic_included = False + conn.close() + + # If the target topic is not found, return False + if not target_topic_included: + return target_topic_included + + # if the target topic is included, establish a connection to an SQLite database to read the target topic + topic_type = topic_types[target_topic_name]["topic_type"] + topic_db3 = db3SingleTopic(self.db3_file, topic_types[target_topic_name]["topic_id"]) + self.db3_dict[target_topic_name] = {"topic_type": topic_type, "topic_db3": topic_db3} + + # Successfully loaded the target topic + return True + + # Fetch a deserialized single message for the specified topic + def read_msg(self, target_topic_name): + msg = None + row = self.db3_dict[target_topic_name]["topic_db3"].fetch_row() + + if row is None: + return msg + + topic_type = self.db3_dict[target_topic_name]["topic_type"] + msg = deserialize_message(row[3], topic_type) + + return msg