导航部分效果最好的版本,主分支

This commit is contained in:
2026-04-13 23:30:22 +08:00
commit 350fd830f4
1679 changed files with 1464081 additions and 0 deletions

169
App/nav/corridor_ctrl.c Normal file
View File

@@ -0,0 +1,169 @@
#include "corridor_ctrl.h"
#include <math.h>
#include <stdbool.h>
/* ====================== 内部状态 ====================== */
static CorridorCtrlConfig_t s_cfg;
static bool s_initialized = false;
/* 辅助:浮点数限幅 */
static inline float clampf(float val, float lo, float hi)
{
if (val < lo) return lo;
if (val > hi) return hi;
return val;
}
void CorridorCtrl_Init(const CorridorCtrlConfig_t *config)
{
s_cfg = *config;
s_initialized = true;
}
void CorridorCtrl_Compute(const CorridorState_t *state,
const CorridorObs_t *obs,
float imu_wz,
RawCmd_t *out_cmd)
{
if (!s_initialized) {
out_cmd->v = 0.0f;
out_cmd->w = 0.0f;
out_cmd->flags = 0U;
return;
}
/* ========================================================
* 出沟保护: 当前激光检测到接近出口时,停止使用左右激光控制
* 避免出沟时左右激光数据突变导致车身大幅度转向
* ======================================================== */
bool near_exit = false;
if ((obs->valid_mask & (1U << 4)) != 0U) { /* 前激光有效 (bit 4) */
if (obs->d_front <= s_cfg.exit_front_dist) {
near_exit = true;
}
}
/* ========================================================
* 核心控制律:
* w_cmd = kp_theta * e_th + kd_theta * (-imu_wz) + kp_y * e_y
*
* - kp_theta * e_th : 比例项,车头偏了就回转
* - kd_theta * (-imu_wz) : 微分阻尼,等价于"阻止车头继续转"
* 用 IMU 直接读数做微分项,比差分 e_th 更丝滑无噪声
* - kp_y * e_y : 横向纠偏,车身偏了就产生角速度拉回来
*
* 出沟保护: 接近出口时,仅使用航向保持,不使用横向和角度纠偏
* ======================================================== */
float w_cmd;
bool escape_active = false;
if (near_exit) {
/* 接近出口: 仅保持航向惯性,禁用左右激光控制 */
w_cmd = -(s_cfg.kd_theta * imu_wz);
} else {
/*
* 简化原则:
* - IMU 只管航向 (e_th + wz)
* - 左右激光只管居中
*
* 只要左右两侧都完整有效,就直接用左右平均距离差做横向误差:
* e_y_direct = (d_right - d_left)/2
* 车在正中时 d_left ~= d_right因此误差应接近 0。
*
* 若当前帧双侧不完整,再退回滤波器给出的 e_y。
*/
float e_y_ctrl = state->e_y;
bool left_ok = ((obs->valid_mask & (1U << 0)) != 0U) &&
((obs->valid_mask & (1U << 1)) != 0U);
bool right_ok = ((obs->valid_mask & (1U << 2)) != 0U) &&
((obs->valid_mask & (1U << 3)) != 0U);
if (left_ok && right_ok) {
float d_left = (obs->d_lf + obs->d_lr) * 0.5f;
float d_right = (obs->d_rf + obs->d_rr) * 0.5f;
e_y_ctrl = 0.5f * (d_right - d_left);
}
/* 正常控制: IMU 管航向, 左右激光管居中 */
w_cmd = -(s_cfg.kp_theta * state->e_th
+ s_cfg.kd_theta * imu_wz
+ s_cfg.kp_y * e_y_ctrl);
/* ========================================================
* 近墙脱离保护:
* 当某一侧平均距离已经明显过小,说明车身已经在擦壁或即将擦壁。
* 此时不能只等 EKF 慢慢回中,直接叠加一个远离墙面的转向保护项。
* ======================================================== */
{
bool left_front_ok = ((obs->valid_mask & (1U << 0)) != 0U);
bool left_rear_ok = ((obs->valid_mask & (1U << 1)) != 0U);
bool right_front_ok = ((obs->valid_mask & (1U << 2)) != 0U);
bool right_rear_ok = ((obs->valid_mask & (1U << 3)) != 0U);
float w_escape = 0.0f;
float left_min = 10.0f;
float right_min = 10.0f;
if (left_front_ok && obs->d_lf < left_min) left_min = obs->d_lf;
if (left_rear_ok && obs->d_lr < left_min) left_min = obs->d_lr;
if (right_front_ok && obs->d_rf < right_min) right_min = obs->d_rf;
if (right_rear_ok && obs->d_rr < right_min) right_min = obs->d_rr;
if (left_min < s_cfg.wall_escape_dist) {
float err = s_cfg.wall_escape_dist - left_min;
w_escape -= s_cfg.wall_escape_kp * err; /* 左侧很近 -> 轻微右转 */
escape_active = true;
}
if (right_min < s_cfg.wall_escape_dist) {
float err = s_cfg.wall_escape_dist - right_min;
w_escape += s_cfg.wall_escape_kp * err; /* 右侧很近 -> 轻微左转 */
escape_active = true;
}
w_escape = clampf(w_escape, -s_cfg.wall_escape_w_max, s_cfg.wall_escape_w_max);
w_cmd += w_escape;
}
}
/* 角速度限幅:防止 PD 溢出导致原地打转 */
w_cmd = clampf(w_cmd, -s_cfg.w_max, s_cfg.w_max);
/* ========================================================
* 线速度策略:
* 基础巡航速度 v_cruise
* 当角速度偏大时适当降速(弯道减速),保持运动学协调
* 公式: v = v_cruise * (1 - k * |w/w_max|)
* k 取 0.3~0.5 较保守
* ======================================================== */
float speed_reduction = s_cfg.speed_reduction_k * fabsf(w_cmd) / s_cfg.w_max;
float v_cmd = s_cfg.v_cruise * (1.0f - speed_reduction);
/* 近墙脱离时轻微降速,避免“贴着墙还继续冲” */
if (escape_active) {
v_cmd *= 0.80f;
}
/* 线速度限幅:不允许倒车,不允许超速 */
v_cmd = clampf(v_cmd, 0.0f, s_cfg.v_max);
/* ========================================================
* 置信度降级保护:
* 当滤波器健康度 conf 过低(两边雷达全瞎),
* 说明走廊参照完全丢失,降低线速度防止盲飞
*
* 注意:阈值不宜过高,否则会过度降级导致控制器失效
* ======================================================== */
if (state->conf < 0.2f) {
/* 健康度极低:速度打三折,保持航向惯性滑行 */
v_cmd *= 0.3f;
} else if (state->conf < 0.4f) {
/* 健康度较低(单侧退化):速度打七折 */
v_cmd *= 0.7f;
}
/* 输出赋值 */
out_cmd->t_ms = state->t_ms;
out_cmd->v = v_cmd;
out_cmd->w = w_cmd;
out_cmd->flags = 0U;
}

52
App/nav/corridor_ctrl.h Normal file
View File

@@ -0,0 +1,52 @@
#ifndef CORRIDOR_CTRL_H
#define CORRIDOR_CTRL_H
#include "preproc/corridor_msgs.h"
/**
* @brief 走廊控制器调参配置
* @note 所有增益参数都应在调试阶段"从小往大调",防止震荡
*/
typedef struct {
float kp_theta; // 航向偏差比例增益 (rad/s per rad)
float kd_theta; // 航向偏差微分增益 (rad/s per rad/s) [阻尼项]
float kp_y; // 横向偏差比例增益 (rad/s per m)
float v_cruise; // 走廊内巡航线速度 (m/s)
float w_max; // 角速度输出硬限幅 (rad/s),超过此值一律削峰
float v_max; // 线速度输出硬限幅 (m/s)
float speed_reduction_k; // 弯道减速系数 (0~1),公式: v = v_cruise*(1-k*|w/w_max|)
float exit_front_dist; // 出沟检测距离 (m),前激光小于此值时禁用左右激光控制
float wall_escape_dist; // 近墙脱离阈值 (m),小于此值触发直接远离墙面
float wall_escape_kp; // 近墙脱离增益 (rad/s per m)
float wall_escape_w_max; // 近墙脱离角速度限幅 (rad/s)
} CorridorCtrlConfig_t;
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief 初始化走廊控制器
* @param config 控制器增益与限幅配置
*/
void CorridorCtrl_Init(const CorridorCtrlConfig_t *config);
/**
* @brief 核心函数:根据滤波器输出的走廊状态计算控制量
* @param state 滤波器输出的走廊状态 (e_y, e_th, conf)
* @param obs 预处理层的观测快照 (用于获取 d_front 等辅助信息)
* @param imu_wz 当前的 IMU Z轴角速度 (rad/s),用于微分阻尼
* @param out_cmd 输出的控制指令 (v, w)
*/
void CorridorCtrl_Compute(const CorridorState_t *state,
const CorridorObs_t *obs,
float imu_wz,
RawCmd_t *out_cmd);
#ifdef __cplusplus
}
#endif
#endif // CORRIDOR_CTRL_H

1051
App/nav/global_nav.c Normal file

File diff suppressed because it is too large Load Diff

156
App/nav/global_nav.h Normal file
View File

@@ -0,0 +1,156 @@
/**
* @file global_nav.h
* @brief 赛道级总控 — 混合导航状态机
*
* 三层架构中的最上层:
* 上层 (本模块): 知道"当前第几沟、下一沟是谁、该往哪转"
* 中层 (内嵌): 转向/连接段/重捕获动作执行
* 下层 (已有): 沟内 corridor_ctrl 闭环
*
* 替代原 nav_script.c 的赛道编排职能。
*/
#ifndef GLOBAL_NAV_H
#define GLOBAL_NAV_H
#include <stdint.h>
#include <stdbool.h>
#include "preproc/corridor_msgs.h"
#include "Contract/robot_blackboard.h"
#include "nav/track_map.h"
#ifdef __cplusplus
extern "C" {
#endif
/* =========================================================
* 赛道级阶段枚举
* ========================================================= */
typedef enum {
GNAV_IDLE = 0, /* 启动区等待 */
GNAV_ENTRY_STRAIGHT, /* 入场直线 */
GNAV_TURN_INTO_CORRIDOR, /* 转向进入垄沟 (原地转 90°) */
GNAV_REACQUIRE, /* 重捕获走廊 */
GNAV_ALIGN, /* 捕获成功后停车对齐航线 */
GNAV_CORRIDOR_TRACK, /* 沟内闭环跟踪 */
GNAV_TURN_OUT_OF_CORRIDOR, /* 到端后出沟转向 (原地转 90°) */
GNAV_LINK_STRAIGHT, /* 连接段直行 */
GNAV_TURN_INTO_NEXT, /* 转向进入下一条沟 (原地转 90°) */
GNAV_EXIT_STRAIGHT, /* 出场直行 */
GNAV_DOCK, /* 回停启动区 */
GNAV_FINISHED, /* 终态 */
GNAV_ERROR /* 异常态 (超时兜底) */
} GlobalNavStage_t;
/* =========================================================
* 配置参数
* ========================================================= */
typedef struct {
/* 入场段 */
float entry_v; /* 入场速度 m/s */
float entry_distance; /* 入场里程上限 m */
uint32_t entry_timeout_ms; /* 入场超时 ms */
/* 转向 */
float turn_omega; /* 转向角速度 rad/s */
float turn_tolerance_rad; /* 转向完成容差 rad */
float turn_decel_zone_rad; /* 接近目标角时的减速区 rad */
float turn_min_omega; /* 减速区最低角速度 rad/s */
uint32_t turn_timeout_ms; /* 单次转向超时 ms */
/* 重捕获 */
float reacquire_v; /* 重捕获速度 m/s */
float reacquire_conf_thresh; /* 重捕获置信度阈值 */
float reacquire_width_tol; /* 走廊宽度容差 m */
float reacquire_min_odom; /* 重捕获最小入沟里程 m */
uint8_t reacquire_confirm_ticks; /* 连续确认拍数 */
uint32_t reacquire_timeout_ms;
/* 对齐 */
float align_kp_th; /* 对齐航向P增益 (rad/s per rad) */
float align_kp_y; /* 对齐横向P增益 (rad/s per m) */
float align_th_tol_rad; /* 对齐航向容差 (rad) */
float align_y_tol_m; /* 对齐横向容差 (m) */
uint8_t align_confirm_ticks; /* 对齐确认拍数 */
uint32_t align_timeout_ms; /* 对齐超时 ms */
float reacquire_min_back_dist; /* 重捕获最小后激光距离 (m),用于判断是否真正进沟 */
/* 沟内 */
float corridor_end_detect_dist; /* 到端检测距离 m */
float corridor_length_max; /* 沟内里程保护上限 m */
/* 连接段 */
float link_v;
float link_distance;
uint32_t link_timeout_ms;
float link_gap_runout; /* 检测到沟口后继续前冲距离 (m) */
float link_wall_target; /* 墙壁跟随目标距离 (m) */
float link_wall_kp; /* 墙壁跟随P增益 */
float link_wall_heading_kp; /* 单边墙平行修正增益 */
float link_wall_blend; /* 墙壁跟随权重 (0~1) */
/* 出场 */
float exit_v; /* 出场速度 m/s */
float exit_runout; /* 出场冲刺距离 m */
float exit_max_dist; /* 出场里程保护 m */
uint32_t exit_timeout_ms;
/* 回停 */
float dock_v; /* 回停速度 m/s */
float dock_distance; /* 回停距离 m */
/* 航向保持 */
float heading_kp; /* 航向保持P增益 */
/* 走廊宽度 (用于重捕获判定) */
float corridor_width; /* 走廊宽度 m */
} GlobalNavConfig_t;
/* =========================================================
* 输出结构
* ========================================================= */
typedef struct {
GlobalNavStage_t stage; /* 当前阶段 */
const char* stage_name; /* 阶段名字符串 (调试) */
uint8_t corridor_id; /* 当前/目标垄沟 ID */
uint8_t corridors_done; /* 已完成垄沟数 */
bool request_corridor; /* 是否请求沟内闭环 */
bool use_override; /* 是否用 override 指令 */
float override_v;
float override_w;
SafetyMode_t safety_mode; /* 当前安全模式 */
bool active; /* 导航是否在运行 */
} GlobalNavOutput_t;
/* =========================================================
* API
* ========================================================= */
void GlobalNav_Init(const GlobalNavConfig_t* cfg);
void GlobalNav_Start(void);
void GlobalNav_Stop(void);
void GlobalNav_Reset(void);
/**
* @brief 核心函数:每个导航周期调用一次
* @param obs 预处理层的观测快照
* @param state EKF 走廊状态
* @param board 黑板快照 (读 IMU yaw_continuous, odom)
* @param now_ms 当前系统时间 (HAL_GetTick),由调用方传入,避免内部依赖 IMU 时间戳
* @param out 导航输出
*/
void GlobalNav_Update(const CorridorObs_t* obs,
const CorridorState_t* state,
const RobotBlackboard_t* board,
uint32_t now_ms,
GlobalNavOutput_t* out);
GlobalNavStage_t GlobalNav_GetStage(void);
const char* GlobalNav_GetStageName(GlobalNavStage_t stage);
#ifdef __cplusplus
}
#endif
#endif /* GLOBAL_NAV_H */

424
App/nav/nav_script.c Normal file
View File

@@ -0,0 +1,424 @@
/**
* @file nav_script.c
* @brief 段脚本执行器实现:编排完整比赛流程
*/
#include "nav_script.h"
#include "est/corridor_filter.h"
#include "robot_params.h"
#include <stdint.h>
#include <stdbool.h>
#include <math.h>
#include <string.h>
/* =========================================================
* 内部状态
* ========================================================= */
static NavScriptConfig_t s_cfg;
static NavScriptStage_t s_stage = SCRIPT_STAGE_IDLE;
static bool s_initialized = false;
static bool s_running = false;
/* 阶段内部状态变量 */
static struct {
uint32_t entry_align_start_ms; // 入口对准开始时间
float turn_start_e_th; // 转向开始时的 EKF 航向 (保留作后备)
float turn_start_imu_yaw_deg; // 转向开始时的 IMU 连续偏航角 (deg)
bool turn_started; // 转向是否已开始
bool turn_coarse_done; // 180° IMU 粗转是否已完成
uint32_t turn_settle_start_ms; // 进入侧墙精调阶段的时刻
float corridor_s_entry; // 进入垄沟时的 s 里程
float end_rearm_s; // 掉头后到端检测重新使能的起始里程
bool end_armed; // 到端检测是否已重新使能
NavScriptStage_t post_turn_stage; // 本次转向完成后要进入的走廊阶段
int pass_count; // 已走过的垄沟数
float exit_start_s; // 离开垄沟瞬间的 s 里程 (0=未触发)
} s_internal;
#define SCRIPT_END_REARM_DIST_M 0.12f
#define SCRIPT_TURN_SETTLE_KP 1.4f
#define SCRIPT_TURN_SETTLE_W_MAX 0.6f
#define SCRIPT_TURN_SETTLE_TOL_RAD 0.05f
#define SCRIPT_TURN_SETTLE_TIMEOUT_MS 1200u
/* =========================================================
* 内部辅助函数
* ========================================================= */
static inline float clampf(float val, float lo, float hi)
{
if (val < lo) return lo;
if (val > hi) return hi;
return val;
}
static bool compute_wall_heading_error(const CorridorObs_t *obs, float *out_heading_rad)
{
float heading_sum = 0.0f;
int valid_count = 0;
const float sensor_base = PARAM_SENSOR_BASE_LENGTH;
bool left_ok = ((obs->valid_mask & CORRIDOR_OBS_MASK_LF) != 0U) &&
((obs->valid_mask & CORRIDOR_OBS_MASK_LR) != 0U);
bool right_ok = ((obs->valid_mask & CORRIDOR_OBS_MASK_RF) != 0U) &&
((obs->valid_mask & CORRIDOR_OBS_MASK_RR) != 0U);
if (left_ok) {
heading_sum += atan2f(obs->d_lr - obs->d_lf, sensor_base);
valid_count++;
}
if (right_ok) {
heading_sum += atan2f(obs->d_rf - obs->d_rr, sensor_base);
valid_count++;
}
if (valid_count == 0) {
return false;
}
*out_heading_rad = heading_sum / (float)valid_count;
return true;
}
/* =========================================================
* API 实现
* ========================================================= */
void NavScript_Init(const NavScriptConfig_t *config)
{
s_cfg = *config;
s_stage = SCRIPT_STAGE_IDLE;
s_running = false;
s_initialized = true;
/* 初始化内部状态 */
memset(&s_internal, 0, sizeof(s_internal));
}
void NavScript_Start(void)
{
if (s_initialized) {
s_stage = SCRIPT_STAGE_ENTRY_ALIGN;
s_running = true;
s_internal.entry_align_start_ms = 0;
s_internal.pass_count = 0;
}
}
NavScriptStage_t NavScript_GetStage(void)
{
return s_stage;
}
const char* NavScript_GetStageName(NavScriptStage_t stage)
{
switch (stage) {
case SCRIPT_STAGE_IDLE: return "IDLE";
case SCRIPT_STAGE_ENTRY_ALIGN: return "ENTRY_ALIGN";
case SCRIPT_STAGE_CORRIDOR_FORWARD: return "CORRIDOR_FWD";
case SCRIPT_STAGE_TURN_AT_END: return "TURN_AT_END";
case SCRIPT_STAGE_CORRIDOR_BACKWARD: return "CORRIDOR_BWD";
case SCRIPT_STAGE_EXIT: return "EXIT";
case SCRIPT_STAGE_FINISHED: return "FINISHED";
default: return "UNKNOWN";
}
}
void NavScript_Reset(void)
{
s_stage = SCRIPT_STAGE_IDLE;
s_running = false;
memset(&s_internal, 0, sizeof(s_internal));
}
void NavScript_Update(const CorridorObs_t *obs,
const CorridorState_t *state,
float imu_yaw_continuous_deg,
NavScriptOutput_t *out)
{
if (!s_initialized) {
out->stage = SCRIPT_STAGE_IDLE;
out->stage_name = "IDLE";
out->active = false;
out->request_corridor = false;
out->override_v = 0.0f;
out->override_w = 0.0f;
out->use_override = true;
return;
}
/* 默认输出 */
out->stage = s_stage;
out->stage_name = NavScript_GetStageName(s_stage);
out->active = s_running;
out->request_corridor = true; // 默认请求走廊控制器
out->use_override = false; // 默认不覆盖
if (!s_running) {
out->request_corridor = false;
out->override_v = 0.0f;
out->override_w = 0.0f;
out->use_override = true;
return;
}
/* ========================================================
* 核心状态机:按比赛流程执行
* ======================================================== */
switch (s_stage) {
/* ==============================================
* STAGE 1: 入口对准
* ============================================== */
case SCRIPT_STAGE_ENTRY_ALIGN: {
if (s_internal.entry_align_start_ms == 0) {
s_internal.entry_align_start_ms = state->t_ms;
}
/* 检查是否已进入垄沟:两侧都有距离数据 */
bool left_ok = ((obs->valid_mask & CORRIDOR_OBS_MASK_LF) != 0U) &&
((obs->valid_mask & CORRIDOR_OBS_MASK_LR) != 0U);
bool right_ok = ((obs->valid_mask & CORRIDOR_OBS_MASK_RF) != 0U) &&
((obs->valid_mask & CORRIDOR_OBS_MASK_RR) != 0U);
if (left_ok && right_ok && state->conf >= 0.8f) {
/* 两侧雷达都有数据,且置信度高 -> 进入垄沟,开始跟踪 */
s_internal.corridor_s_entry = state->s;
s_internal.end_rearm_s = state->s;
s_internal.end_armed = true;
s_internal.pass_count = 1;
s_stage = SCRIPT_STAGE_CORRIDOR_FORWARD;
out->request_corridor = true;
} else {
/* 入口对准策略:慢速前进,让侧向雷达找墙 */
out->override_v = s_cfg.entry_align_v; // 慢速前进 (由 PARAM_SCRIPT_ENTRY_V 配置)
out->override_w = 0.0f;
out->use_override = true;
out->request_corridor = false;
/* P1 修复: 超时保护使用配置参数 s_cfg.entry_align_timeout
* 而非硬编码 30000 ms */
uint32_t elapsed = state->t_ms - s_internal.entry_align_start_ms;
if (elapsed > (uint32_t)s_cfg.entry_align_timeout) {
/* 超时强制进入走廊跟踪:必须补齐与正常入沟一致的内部状态,
* 否则 pass_count 停留在 0导致后续 TURN_AT_END 判定时
* 多跑一趟走廊(三趟而非文档描述的两趟)。 */
s_internal.corridor_s_entry = state->s;
s_internal.end_rearm_s = state->s;
s_internal.end_armed = true;
s_internal.pass_count = 1;
s_stage = SCRIPT_STAGE_CORRIDOR_FORWARD;
}
}
break;
}
/* ==============================================
* STAGE 2: 向前走垄沟
* ============================================== */
case SCRIPT_STAGE_CORRIDOR_FORWARD: {
/* 使用走廊控制器 */
out->request_corridor = true;
if (!s_internal.end_armed) {
if ((state->s - s_internal.end_rearm_s) >= SCRIPT_END_REARM_DIST_M) {
s_internal.end_armed = true;
}
}
/* 检查是否到端 */
bool front_ok = (obs->valid_mask & CORRIDOR_OBS_MASK_FRONT) != 0U;
if (s_internal.end_armed && front_ok && obs->d_front <= s_cfg.d_entry_exit_front) {
/* 前向距离足够近 -> 到达垄沟末端,递增趟数并准备转向 */
s_internal.pass_count++;
s_internal.turn_start_e_th = state->e_th;
s_internal.turn_start_imu_yaw_deg = imu_yaw_continuous_deg;
s_internal.turn_started = false;
s_internal.post_turn_stage = SCRIPT_STAGE_CORRIDOR_BACKWARD;
s_stage = SCRIPT_STAGE_TURN_AT_END;
out->request_corridor = false;
}
break;
}
/* ==============================================
* STAGE 3: 到端原地转向
* ============================================== */
case SCRIPT_STAGE_TURN_AT_END: {
out->request_corridor = false;
if (!s_internal.turn_started) {
s_internal.turn_start_e_th = state->e_th;
s_internal.turn_start_imu_yaw_deg = imu_yaw_continuous_deg;
s_internal.turn_started = true;
s_internal.turn_coarse_done = false;
s_internal.turn_settle_start_ms = 0U;
}
/* 原地 180 度转向策略 */
float target_delta = s_cfg.turn_target_angle; // 默认 PI (180度)
/* ---- 使用 IMU 连续 yaw 判定转角 (deg → rad) ---- *
* 优点IMU 内部维护的 yaw 经过模块校准,比外部积分 wz 更稳定,
* 且 unwrap 后不存在 ±180° 跳变问题。
* 后备:如果 IMU 离线,退化回 EKF e_th 差值判定。 */
float delta_turned;
float imu_delta_deg = imu_yaw_continuous_deg - s_internal.turn_start_imu_yaw_deg;
delta_turned = imu_delta_deg * 0.01745329252f; // deg → rad
float remaining = target_delta - fabsf(delta_turned);
if (!s_internal.turn_coarse_done && remaining <= 0.1f) {
s_internal.turn_coarse_done = true;
s_internal.turn_settle_start_ms = obs->t_ms;
}
if (s_internal.turn_coarse_done) {
float wall_heading = 0.0f;
bool wall_heading_ok = compute_wall_heading_error(obs, &wall_heading);
bool settle_timeout = (obs->t_ms - s_internal.turn_settle_start_ms) >= SCRIPT_TURN_SETTLE_TIMEOUT_MS;
if (wall_heading_ok) {
if (fabsf(wall_heading) <= SCRIPT_TURN_SETTLE_TOL_RAD) {
CorridorFilter_RebaseAfterTurnaround(imu_yaw_continuous_deg * 0.01745329252f);
s_internal.end_rearm_s = state->s;
s_internal.end_armed = false;
s_stage = s_internal.post_turn_stage;
out->override_v = 0.0f;
out->override_w = 0.0f;
out->use_override = true;
} else {
out->override_v = 0.0f;
out->override_w = clampf(-SCRIPT_TURN_SETTLE_KP * wall_heading,
-SCRIPT_TURN_SETTLE_W_MAX,
SCRIPT_TURN_SETTLE_W_MAX);
out->use_override = true;
}
} else if (settle_timeout) {
/* 极端情况下侧墙暂时不可用,超时后接受 IMU 粗转结果,避免卡死。 */
CorridorFilter_RebaseAfterTurnaround(imu_yaw_continuous_deg * 0.01745329252f);
s_internal.end_rearm_s = state->s;
s_internal.end_armed = false;
s_stage = s_internal.post_turn_stage;
out->override_v = 0.0f;
out->override_w = 0.0f;
out->use_override = true;
} else {
out->override_v = 0.0f;
out->override_w = 0.0f;
out->use_override = true;
}
} else {
/* 继续转向:根据目标方向选择角速度 */
float turn_dir = (target_delta > 0.0f) ? 1.0f : -1.0f;
/* 接近目标时减速 */
float turn_speed = s_cfg.turn_omega;
if (remaining < 0.5f) {
turn_speed *= (remaining / 0.5f); // 线性减速
turn_speed = clampf(turn_speed, 0.3f, s_cfg.turn_omega);
}
out->override_v = 0.0f; // 线速度为 0原地转
out->override_w = turn_dir * turn_speed;
out->use_override = true;
}
break;
}
/* ==============================================
* STAGE 4: 返程走垄沟 (原地转 180° 后继续正向行驶)
* ============================================== */
case SCRIPT_STAGE_CORRIDOR_BACKWARD: {
/* 使用走廊控制器,正向行驶 */
out->request_corridor = true;
/* P1 修复: 原地转 180° 后车头已调转,返回方向即"向前"
* 因此到端检测应使用前向雷达 d_front而非后向雷达 d_back */
bool front_ok = (obs->valid_mask & CORRIDOR_OBS_MASK_FRONT) != 0U;
/* 掉头回来时,前向雷达可能还残留近端读数。
* 必须先离开端墙一小段距离,再允许重新触发到端检测。 */
if (!s_internal.end_armed) {
if ((state->s - s_internal.end_rearm_s) >= SCRIPT_END_REARM_DIST_M) {
s_internal.end_armed = true;
}
}
if (s_internal.end_armed && front_ok && obs->d_front <= s_cfg.d_entry_exit_front) {
/* [改进H] 到达另一端: 递增趟数并判断是否退出
* pass_count=1(入沟首趟), =2(返回), =3(第三趟) → EXIT */
s_internal.pass_count++;
if (s_internal.pass_count >= 3) {
/* 走了 3 趟 (去-回-去),进入退出阶段 */
s_stage = SCRIPT_STAGE_EXIT;
s_internal.exit_start_s = 0.0f;
} else {
/* 继续往返: 转向后进入 FORWARD */
s_internal.turn_start_e_th = state->e_th;
s_internal.turn_start_imu_yaw_deg = imu_yaw_continuous_deg;
s_internal.turn_started = false;
s_internal.post_turn_stage = SCRIPT_STAGE_CORRIDOR_FORWARD;
s_stage = SCRIPT_STAGE_TURN_AT_END;
}
out->request_corridor = false;
}
break;
}
/* ==============================================
* STAGE 5: 退出场地
* ============================================== */
case SCRIPT_STAGE_EXIT: {
/* P1 修复: 退出速度使用独立的 exit_v 参数,
* 而非误用角速度参数 turn_omega * 0.5f */
out->request_corridor = false;
out->override_v = s_cfg.exit_v; // 直线冲出速度 (由 PARAM_SCRIPT_EXIT_V 配置)
out->override_w = 0.0f;
out->use_override = true;
/* 检测是否已离开:两侧雷达都丢失 */
bool left_ok = ((obs->valid_mask & CORRIDOR_OBS_MASK_LF) != 0U) &&
((obs->valid_mask & CORRIDOR_OBS_MASK_LR) != 0U);
bool right_ok = ((obs->valid_mask & CORRIDOR_OBS_MASK_RF) != 0U) &&
((obs->valid_mask & CORRIDOR_OBS_MASK_RR) != 0U);
if (!left_ok && !right_ok) {
/* 两侧都丢了 -> 已离开垄沟,再往前冲 exit_runout_m 后停车 */
if (s_internal.exit_start_s == 0.0f) {
s_internal.exit_start_s = state->s;
}
if (state->s - s_internal.exit_start_s >= s_cfg.exit_runout_m) {
/* 冲够了,停车 */
s_stage = SCRIPT_STAGE_FINISHED;
s_internal.exit_start_s = 0.0f;
}
}
break;
}
/* ==============================================
* STAGE 6: 比赛完成,安全停车
* ============================================== */
case SCRIPT_STAGE_FINISHED: {
out->request_corridor = false;
out->override_v = 0.0f;
out->override_w = 0.0f;
out->use_override = true;
s_running = false;
break;
}
/* ==============================================
* DEFAULT/IDLE: 安全停车
* ============================================== */
case SCRIPT_STAGE_IDLE:
default: {
out->request_corridor = false;
out->override_v = 0.0f;
out->override_w = 0.0f;
out->use_override = true;
break;
}
}
}

133
App/nav/nav_script.h Normal file
View File

@@ -0,0 +1,133 @@
/**
* @file nav_script.h
* @brief 段脚本执行器:编排完整比赛流程
*
* 比赛流程模型:
* ┌─────────────┐
* │ ENTRY_ALIGN │ 入口对准:启动区 -> 第一垄沟入口
* └──────┬──────┘
* │
* v
* ┌─────────────┐
* │ CORRIDOR_A │ 第一垄沟跟踪
* └──────┬──────┘
* │ (到端)
* v
* ┌─────────────┐
* │ TURN_180 │ 原地 180 度转向
* └──────┬──────┘
* │
* v
* ┌─────────────┐
* │ CORRIDOR_B │ 返回垄沟跟踪
* └──────┬──────┘
* │ (到端)
* v
* ┌─────────────┐
* │ EXIT │ 退出比赛场地
* └──────┬──────┘
* │
* v
* ┌─────────────┐
* │ FINISH │ 停在启动区
* └─────────────┘
*/
#ifndef NAV_SCRIPT_H
#define NAV_SCRIPT_H
#include <stdint.h>
#include <stdbool.h>
#include "preproc/corridor_msgs.h"
#include "preproc/corridor_preproc.h"
#ifdef __cplusplus
extern "C" {
#endif
/* =========================================================
* 段脚本阶段枚举
* ========================================================= */
typedef enum {
SCRIPT_STAGE_IDLE = 0, // 待命阶段,不输出任何指令
SCRIPT_STAGE_ENTRY_ALIGN, // 入口对准:从启动区到第一垄沟
SCRIPT_STAGE_CORRIDOR_FORWARD, // 向前走垄沟
SCRIPT_STAGE_TURN_AT_END, // 到端原地转向
SCRIPT_STAGE_CORRIDOR_BACKWARD,// 向后走垄沟
SCRIPT_STAGE_EXIT, // 退出场地
SCRIPT_STAGE_FINISHED, // 比赛完成,安全停车
} NavScriptStage_t;
/* =========================================================
* 段脚本配置参数
* ========================================================= */
typedef struct {
float turn_target_angle; // 原地转向目标角度 (rad),默认 PI (180度)
float turn_omega; // 原地转向角速度 (rad/s)
float corridor_length; // 垄沟长度估计 (m),用于触发到端(备用)
float entry_align_timeout; // 入口对准超时 (ms)
float d_entry_exit_front; // 出入口前向距离阈值 (m)
float entry_align_v; // 入口对准慢速前进速度 (m/s)
float exit_runout_m; // 离开走廊后继续冲出的距离 (m)
float exit_v; // 退出场地直线冲出速度 (m/s)
} NavScriptConfig_t;
/* =========================================================
* 段脚本输出
* ========================================================= */
typedef struct {
NavScriptStage_t stage; // 当前执行阶段
const char *stage_name; // 阶段名称字符串
bool active; // 脚本是否正在运行
bool request_corridor; // 是否请求走廊控制器
float override_v; // 覆盖输出的 v (m/s)
float override_w; // 覆盖输出的 w (rad/s)
bool use_override; // 是否使用覆盖值
} NavScriptOutput_t;
/* =========================================================
* API 接口
* ========================================================= */
/**
* @brief 初始化段脚本执行器
*/
void NavScript_Init(const NavScriptConfig_t *config);
/**
* @brief 开始运行比赛脚本
*/
void NavScript_Start(void);
/**
* @brief 核心执行函数:每个导航周期调用一次
* @param obs 预处理层的观测快照
* @param state 滤波器的走廊状态
* @param imu_yaw_continuous_deg IMU unwrap 后的连续偏航角 (度),用于转弯判定
* @param out 段脚本输出
*/
void NavScript_Update(const CorridorObs_t *obs,
const CorridorState_t *state,
float imu_yaw_continuous_deg,
NavScriptOutput_t *out);
/**
* @brief 获取当前执行阶段
*/
NavScriptStage_t NavScript_GetStage(void);
/**
* @brief 获取阶段名称
*/
const char* NavScript_GetStageName(NavScriptStage_t stage);
/**
* @brief 强制重置脚本到 IDLE
*/
void NavScript_Reset(void);
#ifdef __cplusplus
}
#endif
#endif // NAV_SCRIPT_H

165
App/nav/segment_fsm.c Normal file
View File

@@ -0,0 +1,165 @@
#include "segment_fsm.h"
#include <string.h>
/* ====================== 内部状态 ====================== */
static SegFsmConfig_t s_cfg;
static SegFsmState_t s_state = SEG_STATE_IDLE;
static bool s_initialized = false;
/* 辅助:浮点数限幅 */
static inline float seg_clampf(float val, float lo, float hi)
{
if (val < lo) return lo;
if (val > hi) return hi;
return val;
}
void SegFsm_Init(const SegFsmConfig_t *config)
{
s_cfg = *config;
s_state = SEG_STATE_IDLE;
s_initialized = true;
}
void SegFsm_Start(void)
{
if (s_initialized) {
s_state = SEG_STATE_CORRIDOR;
}
}
SegFsmState_t SegFsm_GetState(void)
{
return s_state;
}
const char* SegFsm_GetStateName(SegFsmState_t s)
{
switch (s) {
case SEG_STATE_IDLE: return "IDLE";
case SEG_STATE_CORRIDOR: return "CORRIDOR";
case SEG_STATE_APPROACH: return "APPROACH";
case SEG_STATE_STOP: return "STOP";
case SEG_STATE_ESTOP: return "E-STOP";
default: return "UNKNOWN";
}
}
void SegFsm_Update(const RawCmd_t *raw_cmd,
const CorridorObs_t *obs,
const CorridorState_t *state,
SafetyMode_t mode,
SegFsmOutput_t *out)
{
if (!s_initialized) {
out->state = SEG_STATE_IDLE;
out->safe_v = 0.0f;
out->safe_w = 0.0f;
return;
}
/* ========================================================
* 0. IDLE 模式:零速,不做任何裁剪
* ======================================================== */
if (mode == SAFETY_MODE_IDLE) {
out->state = SEG_STATE_IDLE;
out->safe_v = 0.0f;
out->safe_w = 0.0f;
return;
}
/* ========================================================
* 1. TURN 模式:原地转向,直接放行,不检查前距和置信度
* 解决 RISK-1: 转向阶段不会被安全层卡死
* ======================================================== */
if (mode == SAFETY_MODE_TURN) {
s_state = SEG_STATE_CORRIDOR;
out->state = SEG_STATE_CORRIDOR;
out->safe_v = raw_cmd->v;
out->safe_w = raw_cmd->w;
return;
}
/* ========================================================
* 2. CORRIDOR 模式:完整安全检查 (前向 + 置信度)
* STRAIGHT 模式:前向检查,但不检查置信度
* ======================================================== */
/* 2a. 置信度检查:仅 CORRIDOR 模式下生效 */
if (mode == SAFETY_MODE_CORRIDOR) {
if (state->conf < s_cfg.conf_estop_thresh) {
s_state = SEG_STATE_ESTOP;
}
}
/* 待命状态 */
if (s_state == SEG_STATE_IDLE) {
out->state = SEG_STATE_IDLE;
out->safe_v = 0.0f;
out->safe_w = 0.0f;
return;
}
/* 紧急停车状态:等待置信度恢复 (仅 CORRIDOR 模式可能进入) */
if (s_state == SEG_STATE_ESTOP) {
out->state = SEG_STATE_ESTOP;
out->safe_v = 0.0f;
out->safe_w = 0.0f;
if (state->conf >= 0.5f) {
s_state = SEG_STATE_CORRIDOR;
}
return;
}
/* ========================================================
* 3. 前向距离判定CORRIDOR 和 STRAIGHT 都做
* ======================================================== */
bool front_valid = (obs->valid_mask & CORRIDOR_OBS_MASK_FRONT) != 0U;
float d_front = obs->d_front;
if (front_valid) {
if (d_front <= s_cfg.d_front_stop) {
s_state = SEG_STATE_STOP;
} else if (d_front <= s_cfg.d_front_approach) {
s_state = SEG_STATE_APPROACH;
} else {
if (s_state == SEG_STATE_STOP || s_state == SEG_STATE_APPROACH) {
s_state = SEG_STATE_CORRIDOR;
}
}
}
/* ========================================================
* 4. 根据当前状态输出最终安全速度
* ======================================================== */
switch (s_state) {
case SEG_STATE_CORRIDOR: {
out->safe_v = raw_cmd->v;
out->safe_w = raw_cmd->w;
break;
}
case SEG_STATE_APPROACH: {
float range = s_cfg.d_front_approach - s_cfg.d_front_stop;
float ratio = 1.0f;
if (range > 0.001f) {
ratio = (d_front - s_cfg.d_front_stop) / range;
ratio = seg_clampf(ratio, 0.0f, 1.0f);
}
float v_scaled = s_cfg.approach_min_v + ratio * (raw_cmd->v - s_cfg.approach_min_v);
out->safe_v = seg_clampf(v_scaled, 0.0f, raw_cmd->v);
out->safe_w = raw_cmd->w;
break;
}
case SEG_STATE_STOP:
default: {
out->safe_v = 0.0f;
out->safe_w = 0.0f;
break;
}
}
out->state = s_state;
}

80
App/nav/segment_fsm.h Normal file
View File

@@ -0,0 +1,80 @@
#ifndef SEGMENT_FSM_H
#define SEGMENT_FSM_H
#include "preproc/corridor_msgs.h"
#include "preproc/corridor_preproc.h"
/**
* @brief 段状态机的运行阶段枚举
*/
typedef enum {
SEG_STATE_IDLE = 0, // 待命:不输出任何指令
SEG_STATE_CORRIDOR, // 走廊跟踪中:放行控制器输出
SEG_STATE_APPROACH, // 接近端墙:降速保护
SEG_STATE_STOP, // 到端停车:强制零速
SEG_STATE_ESTOP, // 紧急停车:传感器全部失效或异常
} SegFsmState_t;
/**
* @brief 段状态机配置参数
*/
typedef struct {
float d_front_stop; // 前向停车距离阈值 (m),低于此值直接停车
float d_front_approach; // 前向减速预警距离 (m),低于此值开始线性减速
float approach_min_v; // 减速区内最低速度 (m/s),防止爬行太慢永远到不了
float conf_estop_thresh; // 置信度紧急停车阈值,低于此值触发 E-Stop
} SegFsmConfig_t;
/**
* @brief 段状态机对外暴露的完整快照 (供日志或上位机读取)
*/
typedef struct {
SegFsmState_t state; // 当前状态
float safe_v; // 经安全仲裁后的最终线速度 (m/s)
float safe_w; // 经安全仲裁后的最终角速度 (rad/s)
} SegFsmOutput_t;
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief 初始化段状态机
* @param config 距离阈值与降级参数
*/
void SegFsm_Init(const SegFsmConfig_t *config);
/**
* @brief 启动走廊跟踪(从 IDLE 切换到 CORRIDOR
*/
void SegFsm_Start(void);
/**
* @brief 核心函数:输入控制器的期望指令,输出安全仲裁后的最终指令
* @param raw_cmd 走廊控制器的原始输出 (v, w)
* @param obs 预处理层的观测快照 (提供 d_front 和 valid_mask)
* @param state 滤波器的状态输出 (提供 conf 置信度)
* @param mode 安全模式 (CORRIDOR/TURN/STRAIGHT/IDLE),由赛道级导航指定
* @param out 安全仲裁后的最终输出
*/
void SegFsm_Update(const RawCmd_t *raw_cmd,
const CorridorObs_t *obs,
const CorridorState_t *state,
SafetyMode_t mode,
SegFsmOutput_t *out);
/**
* @brief 获取当前状态机状态 (用于日志打印)
*/
SegFsmState_t SegFsm_GetState(void);
/**
* @brief 获取状态名称字符串 (用于日志打印)
*/
const char* SegFsm_GetStateName(SegFsmState_t s);
#ifdef __cplusplus
}
#endif
#endif // SEGMENT_FSM_H

96
App/nav/track_map.c Normal file
View File

@@ -0,0 +1,96 @@
/**
* @file track_map.c
* @brief 赛道地图 — S 型遍历拓扑表
*
* 正确的场地结构 (参见 Doc/map.md):
* - 垄沟沿 X 轴(横向)分布,长 220cm
* - 左右两端各有纵向端部通道,宽 40cm
* - 启动区在左下角,入口对齐左端通道
* - C1 最靠近入口(南侧)C6 最远(北侧)
*
* S 型路径 (以"北=Y递减=地图上方"为参考):
* 入场→左端通道→右转入C1(→东)→右端到端→左转(朝北)
* →北行70cm→左转入C2(←西)→左端到端→右转(朝北)
* →北行70cm→右转入C3(→东)→...
* →C6(←西)→左端到端→左转(朝南)→南行出场
*
* entry_turn_dir: 从纵向通道(朝北)转入垄沟的方向
* exit_turn_dir: 从垄沟到端后转入纵向通道的方向
*/
#include "track_map.h"
/* =========================================================
* 硬编码 S 型遍历表
*
* 转向方向推导 (北=机器人沿纵向通道前进方向):
* C1: 左端通道朝北→右转→朝东入沟; 到右端→左转→朝北
* C2: 右端通道朝北→左转→朝西入沟; 到左端→右转→朝北
* C3: 左端通道朝北→右转→朝东入沟; 到右端→左转→朝北
* C4: 右端通道朝北→左转→朝西入沟; 到左端→右转→朝北
* C5: 左端通道朝北→右转→朝东入沟; 到右端→左转→朝北
* C6: 右端通道朝北→左转→朝西入沟; 到左端→左转→朝南(出场)
* ========================================================= */
static const TrackMap_t s_map = {
.corridors = {
/* id travel_dir exit_turn entry_turn is_last
* -- ---------- --------- ---------- ------- */
{ 0, TRAVEL_DIR_EAST, TURN_DIR_LEFT, TURN_DIR_RIGHT, false }, /* C1: →东, 从左端右转入, 到右端左转出 */
{ 1, TRAVEL_DIR_WEST, TURN_DIR_RIGHT, TURN_DIR_LEFT, false }, /* C2: ←西, 从右端左转入, 到左端右转出 */
{ 2, TRAVEL_DIR_EAST, TURN_DIR_LEFT, TURN_DIR_RIGHT, false }, /* C3: →东 */
{ 3, TRAVEL_DIR_WEST, TURN_DIR_RIGHT, TURN_DIR_LEFT, false }, /* C4: ←西 */
{ 4, TRAVEL_DIR_EAST, TURN_DIR_LEFT, TURN_DIR_RIGHT, false }, /* C5: →东 */
{ 5, TRAVEL_DIR_WEST, TURN_DIR_LEFT, TURN_DIR_LEFT, true }, /* C6: ←西, 从右端左转入, 到左端左转出(朝南出场) */
},
.entry_corridor_id = 0,
.link_distance_m = TRACK_MAP_LINK_DISTANCE_M,
.corridor_length_m = TRACK_MAP_CORRIDOR_LENGTH_M,
};
/* =========================================================
* API 实现
* ========================================================= */
void TrackMap_Init(void)
{
/* 静态表,无需运行时初始化 */
}
const TrackMap_t* TrackMap_Get(void)
{
return &s_map;
}
const CorridorDescriptor_t* TrackMap_GetCorridor(uint8_t id)
{
if (id >= TRACK_MAP_CORRIDOR_COUNT) {
return &s_map.corridors[0];
}
return &s_map.corridors[id];
}
uint8_t TrackMap_GetNextCorridorId(uint8_t current_id)
{
if (current_id + 1 >= TRACK_MAP_CORRIDOR_COUNT) {
return current_id; /* 已是最后一条 */
}
return current_id + 1;
}
bool TrackMap_IsLastCorridor(uint8_t id)
{
if (id >= TRACK_MAP_CORRIDOR_COUNT) return true;
return s_map.corridors[id].is_last;
}
TurnDirection_t TrackMap_GetExitTurnDir(uint8_t id)
{
if (id >= TRACK_MAP_CORRIDOR_COUNT) return TURN_DIR_LEFT;
return s_map.corridors[id].exit_turn_dir;
}
TurnDirection_t TrackMap_GetEntryTurnDir(uint8_t id)
{
if (id >= TRACK_MAP_CORRIDOR_COUNT) return TURN_DIR_LEFT;
return s_map.corridors[id].entry_turn_dir;
}

91
App/nav/track_map.h Normal file
View File

@@ -0,0 +1,91 @@
/**
* @file track_map.h
* @brief 赛道地图 — 固化 S 型遍历拓扑
*
* 赛道几何 (参见 Doc/map.md):
* - 场地 300cm(X) × 390cm(Y)5 条横向垄背把场地切成 6 条横向垄沟
* - 垄沟沿 X 轴方向,长 220cm宽 40cm
* - 左右两端各有一条纵向端部通道 (宽 40cm长 390cm)
* - 启动区在左下角,入口对齐左端通道
* - 垄沟1(最靠近入口) 到 垄沟6(最远离入口) 自下而上排列
*
* S 型遍历:
* 入场→左端通道→右转入C1(→)→右端到端→左转→北行→左转入C2(←)
* →左端到端→右转→北行→右转入C3(→)→...→C6(←)→左端→左转→南行出场
*
* 地图不做全局坐标定位,只回答三个问题:
* 1. 从第 N 条沟完成后,下一条是第几条?
* 2. 这次该往哪转?(左/右)
* 3. 当前是不是最后一条沟?
*/
#ifndef TRACK_MAP_H
#define TRACK_MAP_H
#include <stdint.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
/* =========================================================
* 常量
* ========================================================= */
#define TRACK_MAP_CORRIDOR_COUNT 6 /* 总共 6 条垄沟 */
#define TRACK_MAP_LINK_DISTANCE_M 0.70f /* 沟间距 (中心到中心) */
#define TRACK_MAP_CORRIDOR_LENGTH_M 2.20f /* 垄沟标称长度 */
/* =========================================================
* 枚举
* ========================================================= */
/** 沟内行驶方向 */
typedef enum {
TRAVEL_DIR_EAST = 0, /* 从左端到右端 (→, +X) 奇数沟 */
TRAVEL_DIR_WEST = 1 /* 从右端到左端 (←, -X) 偶数沟 */
} TravelDirection_t;
/** 转向方向 */
typedef enum {
TURN_DIR_LEFT = +1, /* 逆时针 (CCW), w > 0 */
TURN_DIR_RIGHT = -1 /* 顺时针 (CW), w < 0 */
} TurnDirection_t;
/* =========================================================
* 数据结构
* ========================================================= */
/** 单条垄沟描述 */
typedef struct {
uint8_t id; /* 0-5 */
TravelDirection_t travel_dir; /* 本沟行驶方向 */
TurnDirection_t exit_turn_dir; /* 出沟时的转向方向 */
TurnDirection_t entry_turn_dir; /* 入沟时的转向方向 */
bool is_last; /* 是否为最后一条沟 */
} CorridorDescriptor_t;
/** 完整赛道地图 */
typedef struct {
CorridorDescriptor_t corridors[TRACK_MAP_CORRIDOR_COUNT];
uint8_t entry_corridor_id; /* 入场后第一条沟 = 0 */
float link_distance_m; /* 连接段标称距离 */
float corridor_length_m; /* 垄沟标称长度 */
} TrackMap_t;
/* =========================================================
* API
* ========================================================= */
void TrackMap_Init(void);
const TrackMap_t* TrackMap_Get(void);
const CorridorDescriptor_t* TrackMap_GetCorridor(uint8_t id);
uint8_t TrackMap_GetNextCorridorId(uint8_t current_id);
bool TrackMap_IsLastCorridor(uint8_t id);
TurnDirection_t TrackMap_GetExitTurnDir(uint8_t id);
TurnDirection_t TrackMap_GetEntryTurnDir(uint8_t id);
#ifdef __cplusplus
}
#endif
#endif /* TRACK_MAP_H */