下面给出一份可直接替换到原工程、逐行对照的完整重构方案。
为了便于阅读,我把 5 个“软破坏点”全部拆成 4 个层次:
- 问题简述(一句话)
- 现场症状(如何在日志/现象中识别)
- 根因分析(一句话)
- 可落地的最小修补代码(Copy-Paste 即可)
1. 状态机“顺序跳步”——典型软破坏
问题简述 | case 3 / 4 / 8 缺少 break ,导致连续下落 |
---|
现场症状 | 日志里一条 state=3 的打印后面紧跟着 state=4 、state=5 …… 现场表现为“突然跳关”。 |
根因分析 | C 语法级“隐式贯通”被利用,空 case 成为破坏点。 |
最小修补代码(保持原文件名)
switch (state) {
case 3:
if (g_last_aruco_id == 1) {
sc.Move(0, 0, PID_Yaw.update(M_PI_2 - imu.yaw));
state = 4;
}
break;
case 4:
if (g_last_aruco_id == 2) {
sc.Move(0, 0, PID_Yaw.update(0.0 - imu.yaw));
state = 5;
}
break;
case 8:
doSomething();
break;
}
提示:
- 如果 3、4 两阶段原本没有任何业务逻辑,请把上面
if (g_last_aruco_id …)
作为最小业务保留,防止空 case 再次成为“软破坏”。 - 若是 C++17+,可用
[[fallthrough]];
显式说明有意下落。
2. 障碍避让分支被“挖空”——死分支 + 难以察觉
问题简述 | phase==1 不可达,导致避障闭环断链 |
---|
现场症状 | 机器人卡在接近障碍阶段,永远不绕行;日志里只有 phase=0 的循环打印。 |
根因分析 | 代码里只实现了 phase==0 与 phase==2 ,缺少 0→1→2 的推进条件。 |
最小修补代码(完整 0→1→2 闭环)
switch (phase) {
case 0:
if (laser.min_dist < SAFE_MARGIN) {
sc.Move(0, 0, 0);
phase = 1;
} else {
sc.Move(0.3, 0, 0);
}
break;
case 1:
if (fabs(laser.right - SAFE_MARGIN) < THRESHOLD) {
sc.Move(0, 0, 0);
phase = 2;
} else {
double e = SAFE_MARGIN - laser.right;
sc.Move(0, PID_Lat.update(e), 0);
}
break;
case 2:
static double target_yaw = imu.yaw;
double yaw_err = normalizeAngle(target_yaw - imu.yaw);
if (fabs(yaw_err) < 0.05) {
phase = 0;
} else {
sc.Move(0.1, 0, PID_Yaw.update(yaw_err));
}
break;
}
3. 角度单位“混淆注入”——弧度/度混用
问题简述 | 代码里 90° 既当弧度又当度数 |
---|
现场症状 | 航向 PID 输出剧烈抖动或永远不收敛,角度误差动辄 100°+。 |
根因分析 | angle_delta/180*M_PI 与裸用 89 并存,单位未统一。 |
最小修补代码(全链路弧度化)
constexpr double deg2rad(double d) { return d * M_PI / 180.0; }
constexpr double rad2deg(double r) { return r * 180.0 / M_PI; }
const double TARGET_YAW = deg2rad(90.0);
double err = TARGET_YAW - imu.yaw;
if (fabs(err) > deg2rad(3.0)) {
sc.Move(0, 0, PID_Yaw.update(err));
}
彻底做法:
- 删除所有裸写的
angle * 180 / M_PI
或 angle / 180 * M_PI
。 - 在 CI 中加一条正则检查:
grep -R '180\s*/\s*M_PI\|M_PI\s*/\s*180' src/
直接报错。
4. “阻塞式”旋转回转——硬阻塞 + 无让步
问题简述 | while 紧循环独占线程,导致 IMU 不更新 |
---|
现场症状 | 机器人死转不停,IMU 数据时间戳停止刷新。 |
根因分析 | while(abs(yaw)<M_PI/2) 不 sleep ,阻塞主循环。 |
最小修补代码(非阻塞子状态机)
switch (state) {
case 2:
if (!rotating) {
target_yaw = normalizeAngle(imu.yaw + M_PI_2);
rotating = true;
}
double err = normalizeAngle(target_yaw - imu.yaw);
if (fabs(err) > 0.05) {
sc.Move(0, 0, PID_Yaw.update(err));
} else {
rotating = false;
state = 3;
}
break;
}
5. ArUco 触发条件写反——误触发转向
问题简述 | if (id) 把任意非 0 ID 都当成触发信号 |
---|
现场症状 | 机器人见到任意标签就转向,不按既定顺序。 |
根因分析 | 条件写成 if (g_last_aruco_id) ,而非严格匹配。 |
最小修补代码(精确匹配 + 巡航补位)
switch (state) {
case 3:
if (g_last_aruco_id == expected_id) {
sc.Move(0, 0, PID_Yaw.update(expected_yaw - imu.yaw));
state = 4;
++expected_id;
} else {
sc.Move(0.3, 0, PID_Yaw.update(0.0 - imu.yaw));
}
break;
}
一键替换清单
文件名 | 需改行数 | 关键搜索词 |
---|
state_machine.cpp | 3 处 | case 3: 、case 4: 、case 8: |
obstacle_avoid.cpp | 整段 | phase==0 |
common.h | 追加 | deg2rad |
controller.cpp | 2 处 | 90 、* M_PI / 180 |
turn_logic.cpp | 整段 | while(abs(yaw) |
aruco_trigger.cpp | 整段 | if (g_last_aruco_id) |
把以上 6 个片段整体替换后,CI 跑一次静态分析(clang-tidy + cppcheck),即可彻底消除这 5 个软破坏点。