牵引车-挂车运动学与折刀判定(trailer_kinematics)
这一页解决什么问题
挂车不是「加长版单车」。它的航向是第二个状态量,由铰接点的横向速度驱动, 因此多出一个牵引车没有的现象:折刀(trailer jackknife)。倒车时这个状态还是 不稳定的——方向盘保持一个角度,铰接角就会一直涨到折刀。
这套东西在实车和仿真器里都很贵:要么开 CARLA 等世界跑起来,要么上路试。但它的核心 只是两个一阶微分方程加一个闭式解,完全可以在离线、无图形界面、任何 Python 解释器 上把「余量够不够」算清楚。
本模块因此定位成挂车相关逻辑的回归护栏:不产生画面、不需要 GPU、不订阅外部数据, 只吃参数、吐报告和字符图。
运动学模型
记牵引车后轴处航向为 θ₁、挂车航向为 θ₂、铰接角 φ = θ₁ − θ₂:
θ₁' = v · tan(δ) / L1
θ₂' = ( v·sin(φ) − d·θ₁'·cos(φ) ) / L2
| 符号 | 含义 | 本包默认 |
|---|---|---|
| L1 | 牵引车轴距 | 2.70 m |
| L2 | 铰接点到挂车轴 | 4.20 m |
| d | 铰接点在牵引车后轴后方的偏移(0 即「铰接在轴上」模型) | 0.60 m |
| δ | 前轮转角 | 25° |
| v | 速度(倒车取负) | 1.00 m/s |
积分方式是显式欧拉,步长默认 0.02 s。两个状态量每步都折算回 [-π, π),
避免长时间仿真把角度累加成一圈以上。
稳态有闭式解,拿来当自检
令 φ' = 0,记 A = tan δ / L1,可得
sin(φ) − d·A·cos(φ) = L2·A
写成 R·sin(φ − ψ) = L2·A(R = √(1+(dA)²)、ψ = atan(dA))后解出
φ_steady = atan(dA) + asin( L2·A / √(1 + (dA)²) )
- 默认参数:φ_steady ≈ 52.09°
- 把 d 设成 0:退化为经典的 φ_steady = asin(L2·tan δ / L1) ≈ 46.50°
- 当 L2·A/√(1+(dA)²) > 1 时不存在稳态,铰接角会一直涨到折刀
这个闭式解写进了离线测试(C 组),也写进了「30 秒长仿真是否收敛到 52.09°」这条 检查(C8)。积分代码一旦改坏,测试立刻红。
四条断言
| 断言 | 判据 | 为什么需要 |
|---|---|---|
| 折刀余量 | 最大铰接角 < 折刀角(默认 90°) | 折刀是这类车最致命的状态,必须留余量 |
| 积分精度 | 步长减半后位置差 ≤ check/step_tolerance(默认 1 cm) |
让「误差可忽略」这句话有据可依 |
| 通道宽度 | 全程车身角点横向跨度 ≤ check/channel_width |
决定这个弯能不能过 |
| 倒车稳定性 | 倒车时末段铰接角不得比首段大 1° 以上 | 倒车发散是物理事实,必须显式暴露 |
「通道宽度」的算法:把所有采样点上牵引车与挂车的 8 个角点投影到世界坐标,取 y 方向 (即起步时垂直于车身的方向)的跨度。默认 90° 转弯需要 10.96 m。
参数
| 参数 | 默认 | 说明 |
|---|---|---|
trailer/wheelbase |
2.70 | 牵引车轴距 (m) |
trailer/length |
4.20 | 铰接点到挂车轴 (m) |
trailer/hitch_offset |
0.60 | 铰接点在牵引车后轴后方 (m) |
trailer/max_steer_deg |
35.0 | 前轮最大转角 (度) |
check/jackknife_deg |
90.0 | 折刀角上限 (度) |
check/channel_width |
12.00 | 允许占用的通道宽度 (m) |
check/step_tolerance |
0.01 | 步长减半后的位置差上限 (m) |
sim/speed |
1.00 | 行驶速度 (m/s) |
sim/steer_deg |
25.0 | 前轮转角 (度) |
sim/seconds |
9.10 | 仿真时长 (s),默认正好转 90° |
sim/step |
0.02 | 积分步长 (s) |
sim/gear |
1 | 1 前进、-1 倒车 |
rate/publish_hz |
10.0 | 报告发布后的保活频率 (Hz) |
命令行全部可覆盖,例如 roslaunch trailer_kinematics main.launch steer_deg:=30.0。
话题
| 话题 | 类型 | 说明 |
|---|---|---|
/trailer/report |
std_msgs/String |
完整报告(latched,后连的节点也能拿到) |
/trailer/phase |
std_msgs/String |
铰接角随时间的字符相图(latched) |
/trailer/articulation |
std_msgs/Float64 |
最大铰接角(latched) |
运行与真实输出
catkin_make
source devel/setup.bash
roslaunch trailer_kinematics main.launch
牵引车-挂车运动学与折刀判定(纯几何 + 显式欧拉,不依赖 CARLA)
牵引车:轴距 2.70 m,铰接偏移 0.60 m,最大转角 35.00 度
挂车:铰接点到轴线 4.20 m,组合体约 7.50 m
最小转弯半径:3.856 m
采样:456 个点,时长 9.10 s,速度 1.00 m/s
起点:(0.000, 0.000),终点:(5.800, 5.785)
牵引车转角:90.05 度
铰接角:最大 43.27 度,末段 42.52 度,首段 4.75 度
占用通道宽度:10.96 m
断言:4 通过 / 0 失败(共 4 条)
[PASS] 折刀余量 最大铰接角 43.27 度,折刀角 90.00 度(余量 46.73 度)
[PASS] 积分精度 步长 0.020 与 0.010 的位置差 0.00707 m,上限 0.010 m
[PASS] 通道宽度 占用宽度 10.96 m,通道 12.00 m
[PASS] 倒车稳定性 前进:首段 4.75 度 → 末段 42.52 度,不适用倒车发散判定
结论:PASSED
两个反例
倒车(会折刀):
roslaunch trailer_kinematics main.launch gear:=-1
铰接角:最大 167.36 度,末段 161.78 度,首段 5.46 度
断言:2 通过 / 2 失败(共 4 条)
[FAIL] 折刀余量 最大铰接角 167.36 度,折刀角 90.00 度(余量 -77.36 度)
[PASS] 积分精度 步长 0.020 与 0.010 的位置差 0.00707 m,上限 0.010 m
[PASS] 通道宽度 占用宽度 10.91 m,通道 12.00 m
[FAIL] 倒车稳定性 倒车:首段 5.46 度 → 末段 161.78 度,铰接角持续增大
结论:FAILED
通道不够宽:
roslaunch trailer_kinematics main.launch channel_width:=4.0
只有 [FAIL] 通道宽度 占用宽度 10.96 m,通道 4.00 m 一条失败,其余三条仍然 PASS——
这证明四条断言是各管一件事,不是一起翻红。
离线测试
python3 tests/test_trailer_kinematics_local.py
python3 -m pytest tests/test_trailer_kinematics_local.py -q
只依赖 Python 标准库(不需要 ROS、CARLA、显示器),Melodic 的 Python 2.7 与 Noetic 的 Python 3 都能直接跑。测试覆盖 A 几何参数、B 通用小工具、C 稳态闭式解、 D 仿真推进、E 铰接角指标、F 通道宽度、G 积分精度、H 四条断言、I 字符图、 J 参数读取、K 工程文件约定共 11 组。
与已有模块的分工
| 模块 | 管什么 |
|---|---|
twist_to_control |
Twist 指令 → 转向角与轮速(单车身运动学) |
turning_sweep_check |
单车转弯时车身占多宽 |
reverse_parking_planner |
倒车入库的轨迹规划 |
odom_trace_recorder |
把真实轨迹录下来、回放、与参考比对 |
| 本模块 | 铰接式组合车:第二自由度、折刀余量、倒车稳定性 |
常见问题
为什么默认跑 9.10 秒? L1 = 2.70 m、δ = 25° 时航向角速度是 0.1727 rad/s,转 90° 正好 9.10 s。这样一个默认 场景就是「路口转弯」,通道宽度这个指标才有意义。
倒车为什么一定失败?
不是代码坏了。倒车时铰接角的不动点是不稳定的,方向盘保持一个角度,铰接角就单调
增大到折刀。gear:=-1 存在的意义就是把这条写进证据里。
为什么还要检查积分精度? 显式欧拉在 0.02 s 下误差只有 7 mm,放大到 0.5 s 就变成 17.5 cm。这条断言让文档里 「误差可忽略」的说法有依据,也顺手抓「有人把步长调大图省事」这类回归。
和 CARLA 的 scenario runner 什么关系? CARLA 的 scenario runner 管「整个世界怎么演」,需要仿真器;本模块只管挂车这一个 自由度的运动学和稳定性,任何 Python 都能跑,适合做挂车相关代码的回归护栏。
所使用具体的大模型声明:使用大模型辅助代码编写、测试编写与文档整理,提交内容由本人负责。
TF 广播(ROS 原生形态)
节点算完轨迹后会持续广播变换(tf/enabled 默认 true,频率 tf/rate_hz 默认 10 Hz),
因此除了看终端报告,还可以用 ROS 自带的工具核对位姿:
$ rosrun tf tf_echo map tractor
$ rosrun tf view_frames # 导出一份 tf 树到 frames.pdf
| parent | child | 含义 |
|---|---|---|
map |
map -> tractor |
牵引车后轴在全局地图里的位姿(轨迹终点状态) |
tractor |
trailer |
挂车轴在牵引车坐标系里的位姿(与报告里的铰接角一致) |
默认参数下 map -> tractor 的平移是 (5.800, 5.785, 0.000)、yaw 约 1.5716 rad;
tractor -> trailer 的平移是 (-2.458, 2.879, 0.000)。两张变换的相对航向
-0.7552 rad 正是 trailer_yaw - tractor_yaw,与报告中的 铰接角 最大 43.27° 互相印证。
依赖:tf2_ros、geometry_msgs(package.xml 已声明)。关掉广播用
roslaunch trailer_kinematics main.launch tf_enabled:=false。