Local control algorithm test only

This commit is contained in:
liuhaoyan 2026-10-04 14:40:53 +08:00
parent e301e7cf67
commit 060cf2b078
7 changed files with 38477 additions and 15 deletions

View File

@ -1,5 +1,5 @@
# auto drive misson center webots simulation:
该分支修复了原LQR算法中矩阵错误问题,并对参数进行调教,增加stanley前轮转向控制算法,并适配webots仿真环境。后续实车测试优先使用该分支
lhy本地控制测试分支,无本地控制测试需求请勿使用
## 1, `ubuntu20.04`, `ubuntu22.04`
## 2, 安装 `webots`
`https://github.com/cyberbotics/webots/tags`, 版本: `ubuntu20.04: R2023a, ubuntu22.04:R2025a`

217
path_generate_json.py Normal file
View File

@ -0,0 +1,217 @@
import json
import math
import matplotlib.pyplot as plt
# ============================================================
# 参数配置
# ============================================================
# 起点:采用你提供的第一个路径点
START_LAT = 30.80062100422322
START_LON = 121.21541599992909
TURN_RADIUS = 1.5
MISSION_SPEED = 0.32
ARC_SPACING = 0.3
# 地球半径(m)
EARTH_RADIUS = 6378137.0
#
first_line_length = 6.0
first_arc_rad = math.pi # 180° 半圆
second_line_length = 8.0
second_arc_rad = math.pi / 2 # 90° 四分之一圆
third_line_length = 5.0
# ============================================================
# 工具函数
# ============================================================
def normalize_yaw(yaw):
"""将 yaw 归一化到 [-pi, pi]。"""
return math.atan2(math.sin(yaw), math.cos(yaw))
def local_to_latlon(x, y):
"""
局部 ENU 坐标转经纬度。
x: East,向东为正,单位 m
y: North,向北为正,单位 m
"""
lat0 = math.radians(START_LAT)
lat = START_LAT + math.degrees(y / EARTH_RADIUS)
lon = START_LON + math.degrees(
x / (EARTH_RADIUS * math.cos(lat0))
)
return lat, lon
def add_point(points, x, y, yaw):
"""添加一个 [latitude, longitude, yaw] 路径点。"""
lat, lon = local_to_latlon(x, y)
points.append([
round(lat, 12),
round(lon, 12),
round(normalize_yaw(yaw), 9)
])
def add_arc(points, cx, cy, radius,
start_angle, sweep_angle, spacing):
"""
添加圆弧采样点,不重复添加圆弧起点。
yaw 为圆弧的切线方向,单位 rad。
"""
arc_length = abs(radius * sweep_angle)
num_segments = max(1, math.ceil(arc_length / spacing))
for i in range(1, num_segments + 1):
ratio = i / num_segments
angle = start_angle + sweep_angle * ratio
x = cx + radius * math.cos(angle)
y = cy + radius * math.sin(angle)
# CCW 左转时,切线方向 = 圆心角 + pi/2
yaw = angle + (
math.pi / 2 if sweep_angle > 0
else -math.pi / 2
)
add_point(points, x, y, yaw)
coordinates = []
# 1. 第一段直线:长度 6 m
# 起点 (0, 0),终点 (6, 0)
add_point(coordinates, 0, 0, 0.0)
add_point(coordinates, first_line_length, 0, 0.0)
# 第一段圆弧起始索引
first_turn_index = len(coordinates) - 1
# 2. 第一段半圆:R=1,逆时针 180°
# 圆心 (6, 1),从 -90° 转到 90°
add_arc(
coordinates,
cx=first_line_length,
cy=TURN_RADIUS,
radius=TURN_RADIUS,
start_angle=-math.pi / 2,
sweep_angle= first_arc_rad,
spacing=ARC_SPACING
)
# 3. 第二段直线:长度 8 m
# 半圆终点 (6, 2),直线终点 (-2, 2)
second_line_end_x = first_line_length - second_line_length
second_line_end_y = 2 * TURN_RADIUS
add_point(
coordinates,
second_line_end_x,
second_line_end_y,
math.pi
)
# 第二段圆弧起始索引
second_turn_index = len(coordinates) - 1
# 4. 第二段四分之一圆:顺时针 90°
# 与第一段圆弧方向相反
second_arc_cx = second_line_end_x
second_arc_cy = second_line_end_y + TURN_RADIUS
add_arc(
coordinates,
cx=second_arc_cx,
cy=second_arc_cy,
radius=TURN_RADIUS,
start_angle=-math.pi / 2,
sweep_angle=-second_arc_rad,
spacing=ARC_SPACING
)
# 5. 最后一段直线:长度 5 m
third_line_start_x = second_arc_cx
third_line_start_y = second_arc_cy
add_point(
coordinates,
third_line_start_x - TURN_RADIUS,
third_line_start_y + third_line_length,
math.pi / 2
)
# ============================================================
# 构造符合现有项目的 GeoJSON
# ============================================================
path_json = {
"type": "FeatureCollection",
"properties": {
"turnRadius": TURN_RADIUS,
"misionSpeed": MISSION_SPEED
},
"features": [
{
"type": "Feature",
"properties": {
"turnInds": [
first_turn_index,
second_turn_index
],
"mission": []
},
"geometry": {
"type": "LineString",
"coordinates": coordinates
}
}
]
}
# ============================================================
# 保存文件
# ============================================================
output_file = "test_path.json"
with open(output_file, "w", encoding="utf-8") as f:
json.dump(
path_json,
f,
ensure_ascii=False,
indent=2
)
print(f"JSON 文件已生成: {output_file}")
print(f"路径点数量: {len(coordinates)}")
print(f"turnInds: {path_json['features'][0]['properties']['turnInds']}")
print(f"起点: {coordinates[0]}")
print(f"终点: {coordinates[-1]}")
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot([p[0] for p in coordinates], [p[1] for p in coordinates], "-o", markersize=3, label="Path")
ax.set_xlabel("X (m)")
ax.set_ylabel("Y (m)")
ax.axis("equal")
ax.grid(True)
ax.legend()
plt.tight_layout()
plt.show()

37893
path_visualizer.log Normal file

File diff suppressed because it is too large Load Diff

View File

@ -10,12 +10,12 @@ import matplotlib.pyplot as plt
# 参数
# ============================================================
LOG_PATTERN = "logs/mission_center_logger*.txt"
LOG_PATTERN = "path_visualizer.log"
# 每 5 个完整数据取 1 个
SAMPLE_STEP = 40
SAMPLE_STEP = 1
CSV_FILE = "recalculated_tracking_error.csv"
CSV_FILE = "tracking_error.csv"
# ============================================================
@ -74,6 +74,9 @@ current_path_pattern = re.compile(
r"current_path_yaw:([-+0-9.eE]+)"
)
yaw_command_pattern = re.compile(
r"current_cmd_angular_z:([-+0-9.eE]+)"
)
# ============================================================
# 3. 函数:角度归一化
@ -178,6 +181,7 @@ def calculate_errors(
# ============================================================
all_data = []
yaw_command_data = []
total_raw_count = 0
@ -202,6 +206,7 @@ for file in files:
# --------------------------------------------------------
pending_pose = None
pending_yaw = None
with file.open(
@ -222,7 +227,13 @@ for file in files:
line
)
m_yaw = yaw_command_pattern.search(line)
if m_yaw:
pending_yaw = float(m_yaw.group(1))
if file_data_count % SAMPLE_STEP == 0:
if abs(pending_yaw) >= 2.0:
pending_yaw = 0 # 避免添加接近零的值
yaw_command_data.append(pending_yaw)
if m_pose:
pending_pose = {
@ -351,6 +362,7 @@ for file in files:
pending_pose = None
print(
f"原始完整数据:{file_data_count}"
)
@ -447,6 +459,12 @@ alpha_deg = np.array([
for d in all_data
])
yaw_command = yaw_command_data# 确保长度一致
print(
f"yaw size: {len(yaw_command)}"
)
# ============================================================
# 8. 计算车辆累计行驶距离
@ -524,13 +542,18 @@ with open(
"heading_error_deg",
"yaw_command",
])
for i in range(
len(all_data)
):
if yaw_command[i] is None:
print(f"yaw_command[{i}] is None")
print(f"i = {i}")
print(f"yaw_command length = {len(yaw_command)}")
writer.writerow([
f"{distance_m[i]:.8f}",
@ -545,6 +568,8 @@ with open(
f"{alpha_deg[i]:.8f}",
f"{yaw_command[i]:.8f}"
])
@ -630,7 +655,43 @@ plt.grid(True)
plt.tight_layout()
plt.show()
# ============================================================
# 14. yaw command rad
# ============================================================
plt.figure(
figsize=(12, 5)
)
plt.plot(
distance_m,
yaw_command,
linewidth=1.2
)
plt.axhline(
0.0,
linestyle="--",
linewidth=0.8
)
plt.xlabel(
"Distance (m)"
)
plt.ylabel(
"Yaw Command (rad)"
)
plt.title(
"Recalculated Yaw Command"
)
plt.grid(True)
plt.tight_layout()
plt.show()
# ============================================================
# 15. 车辆轨迹 + 路径点

View File

@ -23,6 +23,10 @@
#include <tf2/LinearMath/Quaternion.h>
#include <std_msgs/msg/int32.hpp>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/rotating_file_sink.h>
class PathVisualizer : public rclcpp::Node
{
public:
@ -38,6 +42,8 @@ private:
double w);
double normalize_angle(double angle);
bool downloadPathJson(const std::string &url,std::string &json_data);
bool downloadPathJson(const std::string &file_path,std::string &json_data,
bool is_local_file);
bool loadPathFromJson(const std::string &json_data);
double gps_to_x(double lat,double lon);
@ -101,7 +107,7 @@ private:
// contorl paramss
double max_speed_ = 0.8;
double max_yaw_rate_ = 2.0;
double lookahead_distance_ =0.6;
double lookahead_distance_ =0.4;
double max_interval_distance_ = 0.2;
double origin_lat_;
double origin_lon_;
@ -113,10 +119,15 @@ private:
bool origin_set_= false;
bool path_transed = false;
std::string file_path_;
std::string file_name_;
std::string path_url_ ;
std::string json_data_;
std::unique_ptr<GeodeticConverter> geodetic_converter_;
// logger
std::shared_ptr<spdlog::logger> logger_;
};
#endif

View File

@ -1,5 +1,7 @@
#include "sweet_webots_robot/PathVisualizer.hpp"
PathVisualizer::PathVisualizer(const std::string& file_name):
Node("path_visualizer"),
target_index_(0),
@ -7,6 +9,7 @@ has_pose_(false)
{
file_name_ = file_name;
path_url_ = "https://test.sweetai.cn:4433/load/locationCmdPath/" + file_name_;
file_path_ = "/home/sweetai/auto_drive_misson_center_webots_simulation/test_path.json";
RCLCPP_INFO(this->get_logger(),"PathVisualizer started");
// pub
cmd_pub_ = this->create_publisher<geometry_msgs::msg::Twist>("/twist_cmd_vel",10);
@ -39,9 +42,35 @@ has_pose_(false)
// urdf pose
tf_broadcaster_ = std::make_unique<tf2_ros::TransformBroadcaster>(*this);
// path get
downloadPathJson(path_url_,json_data_);
downloadPathJson(file_path_,json_data_,true);
loadPathFromJson(json_data_);
// logger初始化
auto console_sink =
std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
auto file_sink =
std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
"./path_visualizer.log",
10 * 1024 * 1024, // 10 MB
5 // 保留5个文件
);
std::vector<spdlog::sink_ptr> sinks{
console_sink,
file_sink
};
logger_ = std::make_shared<spdlog::logger>(
"path_visualizer",
sinks.begin(),
sinks.end()
);
logger_->set_level(spdlog::level::debug);
logger_->info("PathVisualizer started");
}
using json = nlohmann::json;
@ -122,6 +151,60 @@ void PathVisualizer::transformPathToVehicleStart(double vehicle_initial_yaw)
}
}
#include <fstream>
#include <sstream>
#include <string>
bool PathVisualizer::downloadPathJson(const std::string &file_path,
std::string &json_data,
bool is_local_file)
{
json_data.clear();
// 打开本地 JSON 文件
std::ifstream file(file_path);
if (!file.is_open())
{
RCLCPP_ERROR(
this->get_logger(),
"Failed to open JSON file: %s",
file_path.c_str());
return false;
}
// 读取文件全部内容
std::stringstream buffer;
buffer << file.rdbuf();
if (file.bad())
{
RCLCPP_ERROR(
this->get_logger(),
"Failed to read JSON file: %s",
file_path.c_str());
return false;
}
json_data = buffer.str();
if (json_data.empty())
{
RCLCPP_ERROR(
this->get_logger(),
"JSON file is empty: %s",
file_path.c_str());
return false;
}
RCLCPP_INFO(
this->get_logger(),
"Path JSON loaded successfully, size = %zu, path = %s",
json_data.size(),
file_path.c_str());
return true;
}
bool PathVisualizer::downloadPathJson(const std::string &url,
std::string &json_data)
{
@ -171,6 +254,14 @@ bool PathVisualizer::loadPathFromJson(
{
// 解析 JSON
json data =json::parse(json_data);
// 检查 properties
if (!data.contains("properties") ||
!data["properties"].is_object())
{
RCLCPP_ERROR(this->get_logger(),"JSON does not contain valid properties");
return false;
}
max_speed_ = data["properties"]["misionSpeed"].get<double>();
// 检查 features
if (!data.contains("features") ||
!data["features"].is_array() ||
@ -601,10 +692,11 @@ void PathVisualizer::control(){
"control has not pose.");
return;
}
double speed=max_speed_;
publishVehicleInfoRviz();
publishLocalPose();
target_index_= findClosestPoint(forward_index_limit_);
int current_index = target_index_;
int target =findLookAheadPoint();
double target_x =path_x_[target];
@ -617,15 +709,38 @@ void PathVisualizer::control(){
double local_x =cos(yaw_)*dx+sin(yaw_)*dy;
double local_y =-sin(yaw_)*dx+cos(yaw_)*dy;
double target_angle = std::atan2(dy, dx);
double alpha = target_angle - yaw_;
while (alpha > M_PI) alpha -= 2 * M_PI;
while (alpha < -M_PI) alpha += 2 * M_PI;
/*Pure Pursuit*/
double Ld =sqrt(local_x*local_x+local_y*local_y);
double curvature =2.0*local_y/(Ld*Ld);
// double Ld =sqrt(local_x*local_x+local_y*local_y);
// double w_cmd =2.0*speed*sin(alpha)/(Ld);
/*Stanley*/
double curent_path_x = path_x_[current_index];
double curent_path_y = path_y_[current_index];
double current_path_yaw =path_yaw_[current_index];
double dx_path = x_ - curent_path_x;
double dy_path = y_ - curent_path_y;
double error_lat = -dx_path * sin(current_path_yaw) + dy_path * cos(current_path_yaw);
double w_cmd =1.0 * alpha - 1.5 * std::atan2(error_lat,speed);
geometry_msgs::msg::Twist cmd;
double speed=max_speed_;
cmd.linear.x =speed;
cmd.angular.z =speed*curvature;
logger_->debug("current_pose_x:{}, current_pose_y:{},current_pose_yaw:{}",
x_, y_,yaw_
);
logger_->debug("current_path_x:{}, current_path_y:{},current_path_yaw:{}",
target_x,
target_y,
target_yaw
);
cmd.linear.x = speed;
cmd.angular.z = w_cmd;
if(cmd.angular.z >max_yaw_rate_)
{
@ -637,7 +752,8 @@ void PathVisualizer::control(){
cmd.angular.z=-max_yaw_rate_;
}
//cmd_pub_->publish(cmd);
cmd_pub_->publish(cmd);
logger_->debug("current_cmd_angular_z:{}",cmd.angular.z);
// RCLCPP_INFO_THROTTLE(
// this->get_logger(),

164
test_path.json Normal file
View File

@ -0,0 +1,164 @@
{
"type": "FeatureCollection",
"properties": {
"turnRadius": 1.5,
"misionSpeed": 0.32
},
"features": [
{
"type": "Feature",
"properties": {
"turnInds": [
1,
18
],
"mission": []
},
"geometry": {
"type": "LineString",
"coordinates": [
[
30.800621004223,
121.215415999929,
0.0
],
[
30.800621004223,
121.215478749384,
0.0
],
[
30.800621263136,
121.215481809837,
0.196349541
],
[
30.800622029926,
121.215484752678,
0.392699082
],
[
30.800623275125,
121.215487464816,
0.589048623
],
[
30.80062495088,
121.215489842025,
0.785398163
],
[
30.800626992794,
121.21549179295,
0.981747704
],
[
30.800629322397,
121.215493242618,
1.178097245
],
[
30.800631850163,
121.215494135319,
1.374446786
],
[
30.800634478952,
121.215494436748,
1.570796327
],
[
30.800637107742,
121.215494135319,
1.767145868
],
[
30.800639635508,
121.215493242618,
1.963495408
],
[
30.800641965111,
121.21549179295,
2.159844949
],
[
30.800644007025,
121.215489842025,
2.35619449
],
[
30.80064568278,
121.215487464816,
2.552544031
],
[
30.800646927979,
121.215484752678,
2.748893572
],
[
30.800647694769,
121.215481809837,
2.945243113
],
[
30.800647953682,
121.215478749384,
3.141592654
],
[
30.800647953682,
121.215395083444,
3.141592654
],
[
30.800648212595,
121.215392022991,
2.945243113
],
[
30.800648979384,
121.21538908015,
2.748893572
],
[
30.800650224583,
121.215386368012,
2.552544031
],
[
30.800651900339,
121.215383990803,
2.35619449
],
[
30.800653942253,
121.215382039878,
2.159844949
],
[
30.800656271855,
121.21538059021,
1.963495408
],
[
30.800658799622,
121.215379697509,
1.767145868
],
[
30.800661428411,
121.21537939608,
1.570796327
],
[
30.800706344175,
121.21537939608,
1.570796327
]
]
}
}
]
}