Ardupilot開源飛控之AP_Follow
- 1. 源由
- 2. 定義
- 2.1 ModeFollow類
- 2.1.1 ModeFollow::update
- 2.1.2 ModeFollow::_enter
- 2.1.3 ModeFollow::_exit
- 2.2 AP_Follow類
- 2.2.1 AP_Follow::handle_msg
- 2.2.2 AP_Follow::get_target_location_and_velocity
- 2.2.3 AP_Follow::get_velocity_ned
- 3. 其他函數
- 3.1 JitterCorrection::correct_offboard_timestamp_msec
- 4. 總結
- 5. 參考資料
1. 源由
前面在《Ardupilot開源飛控之FollowMe計劃》最初“【無障礙物】主動FollowMe功能(采用GPS) ”就是在這部分實現的。
那么我們研讀下這部分代碼。
2. 定義
關于模型模式的相關框架,詳見:ArduPilot開源飛控之飛行模式。注:ArduRover和ArduCopter類似,可以相互借鑒。
2.1 ModeFollow類
ModeFollow
├── 繼承自 Mode
├── 公有成員函數 (public)
│ ├── mode_number() const
│ ├── name4() const
│ ├── update() override
│ ├── is_autopilot_mode() const
│ ├── wp_bearing() const
│ ├── nav_bearing() const
│ ├── crosstrack_error() const
│ ├── get_desired_location(Location& destination) const WARN_IF_UNUSED
│ ├── get_distance_to_destination() const
│ └── set_desired_speed(float speed)
└── 保護成員函數與變量 (protected)├── _enter() override├── _exit() override└── _desired_speed
-
類定義
class ModeFollow : public Mode
ModeFollow
類繼承自Mode
類。
-
公有成員函數(public)
Number mode_number() const override
- 返回模式編號,具體為
Number::FOLLOW
。
- 返回模式編號,具體為
const char *name4() const override
- 返回模式名稱,為 “FOLL”。
void update() override
- 更新車輛狀態的方法。
bool is_autopilot_mode() const override
- 返回是否為自動駕駛模式,這里返回
true
。
- 返回是否為自動駕駛模式,這里返回
float wp_bearing() const override
- 返回導航點的航向。
float nav_bearing() const override
- 返回導航航向,這里調用
wp_bearing()
方法。
- 返回導航航向,這里調用
float crosstrack_error() const override
- 返回偏航誤差,這里固定返回
0.0f
。
- 返回偏航誤差,這里固定返回
bool get_desired_location(Location& destination) const override WARN_IF_UNUSED
- 返回期望的位置,這里固定返回
false
。
- 返回期望的位置,這里固定返回
float get_distance_to_destination() const override
- 返回到目的地的距離。
bool set_desired_speed(float speed) override
- 設置期望的速度。
-
保護成員函數(protected)
bool _enter() override
- 進入模式時的操作。
void _exit() override
- 退出模式時的操作。
-
保護成員變量(protected)
float _desired_speed
- 期望速度,單位為 m/s。
2.1.1 ModeFollow::update
模式更新函數,定時輪訓。
SCHED_TASK(update_current_mode, 400, 200, 12),└──> Rover::update_current_mode└──> control_mode->update└──> ModeFollow::update
主要分為以下幾個步驟:
- 聲明需要的變量。
- 獲取并檢查當前車輛的速度,如果無法獲取有效速度則停止車輛。
- 獲取目標車輛的距離和速度信息,如果無法獲取則停止車輛。
- 計算期望速度向量。
- 檢查期望速度是否為零,如果為零則停止車輛。
- 設置未到達目的地狀態。
- 調整期望速度以保持在預設的速度限制內。
- 計算車輛的期望航向。
- 根據期望航向和速度控制車輛的轉向和油門。
ModeFollow::update()
├── 聲明變量
│ ├── float speed;
│ ├── Vector3f dist_vec;
│ ├── Vector3f dist_vec_offs;
│ ├── Vector3f vel_of_target;
│ ├── Vector2f desired_velocity_ne;
│ └── float desired_speed;
|
├── 獲取車輛速度并檢查
│ ├── if (!attitude_control.get_forward_speed(speed))
│ │ ├── g2.motors.set_throttle(0.0f);
│ │ ├── g2.motors.set_steering(0.0f);
│ │ └── return;
|
├── 獲取目標距離和速度并檢查
│ ├── if (!g2.follow.get_target_dist_and_vel_ned(dist_vec, dist_vec_offs, vel_of_target))
│ │ ├── _reached_destination = true;
│ │ ├── stop_vehicle();
│ │ └── return;
|
├── 計算期望速度向量
│ ├── const float kp = g2.follow.get_pos_p().kP();
│ ├── desired_velocity_ne.x = vel_of_target.x + (dist_vec_offs.x * kp);
│ ├── desired_velocity_ne.y = vel_of_target.y + (dist_vec_offs.y * kp);
|
├── 檢查期望速度是否為零
│ ├── if (is_zero(desired_velocity_ne.x) && is_zero(desired_velocity_ne.y))
│ │ ├── _reached_destination = true;
│ │ ├── stop_vehicle();
│ │ └── return;
|
├── 設置未到達目的地狀態
│ ├── _reached_destination = false;
|
├── 縮放期望速度以保持在水平速度限制內
│ ├── desired_speed = safe_sqrt(sq(desired_velocity_ne.x) + sq(desired_velocity_ne.y));
│ ├── if (!is_zero(desired_speed) && (desired_speed > _desired_speed))
│ │ ├── const float scalar_xy = _desired_speed / desired_speed;
│ │ ├── desired_velocity_ne *= scalar_xy;
│ │ └── desired_speed = _desired_speed;
|
├── 計算車輛航向
│ ├── const float desired_yaw_cd = wrap_180_cd(atan2f(desired_velocity_ne.y, desired_velocity_ne.x) * DEGX100);
|
├── 運行轉向和油門控制器
│ ├── calc_steering_to_heading(desired_yaw_cd);
│ └── calc_throttle(desired_speed, true);
2.1.2 ModeFollow::_enter
進入模式,執行的初始化函數。
Rover::set_mode└──> Mode::enter└──> ModeFollow::_enter
判斷跟隨模式是否使能,使能情況下初始化期望速度。
// initialize follow mode
bool ModeFollow::_enter()
{if (!g2.follow.enabled()) {return false;}// initialise speed to waypoint speed_desired_speed = g2.wp_nav.get_default_speed();return true;
}
2.1.3 ModeFollow::_exit
退出模式,執行的“清場”函數。
Rover::set_mode└──> Mode::exit└──> ModeFollow::_exit
清場處理。
// exit handling
void ModeFollow::_exit()
{g2.follow.clear_offsets_if_required();
}
2.2 AP_Follow類
AP_Follow
類用于管理無人機的跟隨模式,包含目標位置追蹤、偏移處理、航向控制等功能。
AP_Follow
├── 枚舉類型
│ ├── Option
│ └── YawBehave
├── 構造函數
│ └── AP_Follow()
├── 靜態方法
│ └── get_singleton()
├── 公有方法
│ ├── enabled() const
│ ├── set_target_sysid(uint8_t sysid)
│ ├── clear_offsets_if_required()
│ ├── have_target() const
│ ├── get_target_location_and_velocity(Location &loc, Vector3f &vel_ned) const
│ ├── get_target_location_and_velocity_ofs(Location &loc, Vector3f &vel_ned) const
│ ├── get_target_dist_and_vel_ned(Vector3f &dist_ned, Vector3f &dist_with_ofs, Vector3f &vel_ned)
│ ├── get_target_sysid() const
│ ├── get_pos_p() const
│ ├── get_yaw_behave() const
│ ├── get_target_heading_deg(float &heading) const
│ ├── handle_msg(const mavlink_message_t &msg)
│ ├── get_distance_to_target() const
│ ├── get_bearing_to_target() const
│ ├── get_last_update_ms() const
│ ├── option_is_enabled(Option option) const
│ └── var_info[]
├── 靜態成員
│ └── _singleton
├── 私有方法
│ ├── get_velocity_ned(Vector3f &vel_ned, float dt) const
│ ├── init_offsets_if_required(const Vector3f &dist_vec_ned)
│ ├── get_offsets_ned(Vector3f &offsets) const
│ ├── rotate_vector(const Vector3f &vec, float angle_deg) const
│ └── clear_dist_and_bearing_to_target()
└── 私有成員變量├── _enabled├── _sysid├── _dist_max├── _offset_type├── _offset├── _yaw_behave├── _alt_type├── _p_pos├── _options├── _last_location_update_ms├── _target_location├── _target_velocity_ned├── _target_accel_ned├── _last_heading_update_ms├── _target_heading├── _automatic_sysid├── _dist_to_target├── _bearing_to_target└── _offsets_were_zero
- 公有成員
-
枚舉類型
Option
和YawBehave
Option
枚舉:定義跟隨選項參數。YawBehave
枚舉:定義航向行為參數。
-
構造函數
AP_Follow()
: 類的構造函數。
-
靜態方法
get_singleton()
: 返回單例對象。
-
公有方法
enabled() const
: 返回庫是否啟用。set_target_sysid(uint8_t sysid)
: 設置跟隨目標的系統ID。clear_offsets_if_required()
: 如有必要,重置偏移量。have_target() const
: 返回是否有有效的目標位置估計。get_target_location_and_velocity(Location &loc, Vector3f &vel_ned) const
: 獲取目標位置和速度。get_target_location_and_velocity_ofs(Location &loc, Vector3f &vel_ned) const
: 獲取目標位置和速度(包含偏移)。get_target_dist_and_vel_ned(Vector3f &dist_ned, Vector3f &dist_with_ofs, Vector3f &vel_ned)
: 獲取到目標的距離和速度。get_target_sysid() const
: 獲取目標系統ID。get_pos_p() const
: 獲取位置控制器。get_yaw_behave() const
: 獲取用戶定義的航向行為。get_target_heading_deg(float &heading) const
: 獲取目標航向。handle_msg(const mavlink_message_t &msg)
: 解析包含目標位置、速度和姿態的MAVLink消息。get_distance_to_target() const
: 獲取到目標的水平距離。get_bearing_to_target() const
: 獲取到目標的方位。get_last_update_ms() const
: 獲取最后一次位置更新的系統時間。option_is_enabled(Option option) const
: 返回是否啟用某個跟隨選項。var_info[]
: 參數列表。
- 私有成員
-
靜態成員
_singleton
: 單例對象指針。
-
私有方法
get_velocity_ned(Vector3f &vel_ned, float dt) const
: 獲取NED坐標系中的速度估計。init_offsets_if_required(const Vector3f &dist_vec_ned)
: 初始化偏移量。get_offsets_ned(Vector3f &offsets) const
: 獲取NED坐標系中的偏移量。rotate_vector(const Vector3f &vec, float angle_deg) const
: 順時針旋轉3D向量。clear_dist_and_bearing_to_target()
: 重置到目標的距離和方位。
-
私有成員變量
_enabled
: 是否啟用該子系統。_sysid
: 目標的MAVLink系統ID。_dist_max
: 到目標的最大距離,超出此距離的目標將被忽略。_offset_type
: 偏移坐標系類型。_offset
: 與目標的偏移量。_yaw_behave
: 跟隨車輛的航向行為。_alt_type
: 跟隨模式下的高度源。_p_pos
: 位置誤差P控制器。_options
: 跟隨模式的選項。_last_location_update_ms
: 最后一次位置更新的系統時間。_target_location
: 目標的最后已知位置。_target_velocity_ned
: 目標在NED坐標系中的速度。_target_accel_ned
: 目標在NED坐標系中的加速度。_last_heading_update_ms
: 最后一次航向更新的系統時間。_target_heading
: 目標的航向。_automatic_sysid
: 是否自動鎖定系統ID。_dist_to_target
: 到目標的最新距離。_bearing_to_target
: 到目標的最新方位。_offsets_were_zero
: 偏移量是否最初為零然后初始化為與目標的偏移量。_jitter
: 抖動校正器,最大傳輸延遲為3秒。
2.2.1 AP_Follow::handle_msg
AP_Follow::handle_msg(const mavlink_message_t &msg)
|
|-- 初步檢查
| |-- if (!_enabled) //未使能`AP_Follow`類
| | |-- return;
| |
| |-- if (msg.sysid == mavlink_system.sysid) //自身MAVLink消息忽略
| | |-- return;
| |
| |-- if (_sysid != 0 && msg.sysid != _sysid) //非跟蹤目標MAVLink消息忽略
| |-- if (_automatic_sysid) // 使能超時自動重置跟蹤目標
| |-- if ((_last_location_update_ms == 0) || (AP_HAL::millis() - _last_location_update_ms > AP_FOLLOW_SYSID_TIMEOUT_MS))
| |-- _sysid.set(0);
| |-- return;
|
|-- 消息解析
| |-- bool updated = false;
|
| |-- switch (msg.msgid)
| |
| |-- case MAVLINK_MSG_ID_GLOBAL_POSITION_INT
| | |-- mavlink_global_position_int_t packet;
| | |-- mavlink_msg_global_position_int_decode(&msg, &packet);
| |
| | |-- if ((packet.lat == 0 && packet.lon == 0))
| | |-- return;
| |
| | |-- _target_location.lat = packet.lat;
| | |-- _target_location.lng = packet.lon;
| |
| | |-- if (_alt_type == AP_FOLLOW_ALTITUDE_TYPE_RELATIVE)
| | |-- _target_location.set_alt_cm(packet.relative_alt / 10, Location::AltFrame::ABOVE_HOME);
| | | else
| | |-- _target_location.set_alt_cm(packet.alt / 10, Location::AltFrame::ABSOLUTE);
| |
| | |-- _target_velocity_ned.x = packet.vx * 0.01f;
| | |-- _target_velocity_ned.y = packet.vy * 0.01f;
| | |-- _target_velocity_ned.z = packet.vz * 0.01f;
| |
| | |-- _last_location_update_ms = _jitter.correct_offboard_timestamp_msec(packet.time_boot_ms, AP_HAL::millis());
| | |-- if (packet.hdg <= 36000)
| | |-- _target_heading = packet.hdg * 0.01f;
| | |-- _last_heading_update_ms = _last_location_update_ms;
| |
| | |-- if (_sysid == 0)
| | |-- _sysid.set(msg.sysid);
| | |-- _automatic_sysid = true;
| |
| | |-- updated = true;
| | |-- break;
|
| |-- case MAVLINK_MSG_ID_FOLLOW_TARGET
| | |-- mavlink_follow_target_t packet;
| | |-- mavlink_msg_follow_target_decode(&msg, &packet);
| |
| | |-- if ((packet.lat == 0 && packet.lon == 0))
| | |-- return;
| |
| | |-- if ((packet.est_capabilities & (1<<0)) == 0)
| | |-- return;
| |
| | |-- Location new_loc = _target_location;
| | |-- new_loc.lat = packet.lat;
| | |-- new_loc.lng = packet.lon;
| | |-- new_loc.set_alt_cm(packet.alt * 100, Location::AltFrame::ABSOLUTE);
| |
| | |-- if (_alt_type == AP_FOLLOW_ALTITUDE_TYPE_RELATIVE && !new_loc.change_alt_frame(Location::AltFrame::ABOVE_HOME))
| | |-- return;
| |
| | |-- _target_location = new_loc;
| |
| | |-- if (packet.est_capabilities & (1<<1))
| | |-- _target_velocity_ned.x = packet.vel[0];
| | |-- _target_velocity_ned.y = packet.vel[1];
| | |-- _target_velocity_ned.z = packet.vel[2];
| | | else
| | |-- _target_velocity_ned.zero();
| |
| | |-- _last_location_update_ms = _jitter.correct_offboard_timestamp_msec(packet.timestamp, AP_HAL::millis());
| |
| | |-- if (packet.est_capabilities & (1<<3))
| | |-- Quaternion q{packet.attitude_q[0], packet.attitude_q[1], packet.attitude_q[2], packet.attitude_q[3]};
| | |-- float r, p, y;
| | |-- q.to_euler(r, p, y);
| | |-- _target_heading = degrees(y);
| | |-- _last_heading_update_ms = _last_location_update_ms;
| |
| | |-- if (_sysid == 0)
| | |-- _sysid.set(msg.sysid);
| | |-- _automatic_sysid = true;
| |
| | |-- updated = true;
| | |-- break;
|
|-- if (updated)
| |-- #if HAL_LOGGING_ENABLED
| |-- Location loc_estimate{};
| |-- Vector3f vel_estimate;
| |-- UNUSED_RESULT(get_target_location_and_velocity(loc_estimate, vel_estimate));
| |
| |-- AP::logger().WriteStreaming("FOLL",
| "TimeUS,Lat,Lon,Alt,VelN,VelE,VelD,LatE,LonE,AltE", // labels
| "sDUmnnnDUm", // units
| "F--B000--B", // mults
| "QLLifffLLi", // fmt
| AP_HAL::micros64(),
| _target_location.lat,
| _target_location.lng,
| _target_location.alt,
| (double)_target_velocity_ned.x,
| (double)_target_velocity_ned.y,
| (double)_target_velocity_ned.z,
| loc_estimate.lat,
| loc_estimate.lng,
| loc_estimate.alt);
| |-- #endif
-
初步檢查:
- 啟用檢查:如果未啟用
AP_Follow
類,則直接返回。 - 跳過自有消息:如果消息來自當前系統(即自反饋消息),則跳過。
- 目標系統檢查:如果消息不來自當前目標系統且不是自動系統ID,則跳過,并在必要時重置系統ID。
- 啟用檢查:如果未啟用
-
消息解析:
- 消息類型判斷:根據消息的
msgid
,分別處理不同類型的MAVLink消息:MAVLINK_MSG_ID_GLOBAL_POSITION_INT
:處理全局位置消息,更新目標的經緯度、高度、速度和航向信息。如果消息無效(經緯度為零),則忽略消息。MAVLINK_MSG_ID_FOLLOW_TARGET
:處理跟蹤目標消息,更新目標的經緯度、高度、速度和航向信息。如果消息無效(經緯度為零或不包含位置數據),則忽略消息。
- 消息類型判斷:根據消息的
-
更新檢查和日志記錄:
- 如果目標數據被更新,則在啟用日志記錄時記錄目標的估計位置和速度,以及車輛的當前位置和速度。
該方法通過處理不同類型的MAVLink消息來跟蹤目標的實時位置和運動信息,并在需要時進行日志記錄,以確保目標跟蹤的精度和可靠性。
2.2.2 AP_Follow::get_target_location_and_velocity
get_target_location_and_velocity(Location &loc, Vector3f &vel_ned) -> bool
└── 判斷是否啟用 (_enabled)├── 否:立即返回 false└── 是:繼續└── 檢查是否超時├── 是:立即返回 false└── 否:繼續└── 計算自上次位置更新后的時間差 (dt)└── 獲取速度估算 (get_velocity_ned)├── 失敗:返回 false└── 成功:繼續└── 投影車輛位置 (_target_location)├── 使用速度 (vel_ned.x 和 vel_ned.y) 更新水平位置└── 使用速度 (vel_ned.z) 更新垂直位置└── 更新后的位置信息 (last_loc) 賦值給 loc└── 返回 true
-
判斷是否啟用:
- 檢查
_enabled
是否為true
。 - 如果
_enabled
為false
,函數立即返回false
。
- 檢查
-
檢查是否超時:
- 檢查
_last_location_update_ms
是否為0
,或當前時間與_last_location_update_ms
的差值是否超過AP_FOLLOW_TIMEOUT_MS
。 - 如果是超時條件,則函數立即返回
false
。
- 檢查
-
計算時間差 (dt):
- 計算自上次位置更新以來的時間差,單位為秒。
-
獲取速度估算:
- 調用
get_velocity_ned(vel_ned, dt)
獲取速度估算值。 - 如果獲取失敗,函數返回
false
。
- 調用
-
投影車輛位置:
- 根據當前速度估算值和時間差,更新目標位置。
- 水平方向:
vel_ned.x * dt
和vel_ned.y * dt
用于更新水平位置。 - 垂直方向:
vel_ned.z * 100.0f * dt
用于更新垂直位置(速度單位轉換為 cm/s,并乘以時間差)。 - 更新后的目標位置賦值給
loc
。
-
返回最新的位置信息:
- 函數成功執行完畢,返回
true
。
- 函數成功執行完畢,返回
2.2.3 AP_Follow::get_velocity_ned
AP_Follow: Calculate acceleration target #20922
// get velocity estimate in m/s in NED frame using dt since last update
bool AP_Follow::get_velocity_ned(Vector3f &vel_ned, float dt) const
{vel_ned = _target_velocity_ned + (_target_accel_ned * dt);return true;
}
3. 其他函數
3.1 JitterCorrection::correct_offboard_timestamp_msec
JitterCorrection::correct_offboard_timestamp_msec└──> correct_offboard_timestamp_useccorrect_offboard_timestamp_usec(uint64_t offboard_usec, uint64_t local_usec)
|
|-- 計算本地時間與外部時間的差異
| |
| |-- int64_t diff_us = int64_t(local_usec) - int64_t(offboard_usec);
|
|-- 初次初始化或時間差異超前的處理
| |
| |-- if (!initialised || diff_us < link_offset_usec)
| |
| |-- 設置 link_offset_usec 為當前差異
| |-- link_offset_usec = diff_us;
| |-- 標記為已初始化
| |-- initialised = true;
|
|-- 估算校正后的外部時間
| |
| |-- int64_t estimate_us = offboard_usec + link_offset_usec;
|
|-- 檢查消息是否太過于滯后
| |
| |-- if (estimate_us + (max_lag_ms*1000U) < int64_t(local_usec))
| |
| |-- 將估算時間調整為最大滯后時間
| |-- estimate_us = local_usec - (max_lag_ms*1000U);
| |-- 更新 link_offset_usec
| |-- link_offset_usec = estimate_us - offboard_usec;
|
|-- 最小樣本處理
| |
| |-- if (min_sample_counter == 0)
| |
| |-- 初始化 min_sample_us
| |-- min_sample_us = diff_us;
|
| |-- 增加樣本計數器
| | |-- min_sample_counter++;
|
| |-- 如果當前差異小于最小樣本,更新最小樣本
| | |-- if (diff_us < min_sample_us)
| |-- min_sample_us = diff_us;
|
| |-- 達到收斂循環次數后,更新 link_offset_usec
| | |-- if (min_sample_counter == convergence_loops)
| |
| |-- 更新 link_offset_usec 為最小樣本
| |-- link_offset_usec = min_sample_us;
| |-- 重置樣本計數器
| |-- min_sample_counter = 0;
|
|-- 返回校正后的時間
| |-- return uint64_t(estimate_us);
-
計算時間差異:
diff_us
計算本地時間local_usec
和外部時間offboard_usec
之間的差異。
-
初次初始化或時間差異超前的處理:
- 如果還未初始化或時間差異小于
link_offset_usec
,則更新link_offset_usec
并標記為已初始化。這部分處理外部時間戳過于超前的情況。
- 如果還未初始化或時間差異小于
-
估算校正后的外部時間:
- 使用外部時間
offboard_usec
加上link_offset_usec
來估算校正后的時間estimate_us
。
- 使用外部時間
-
檢查消息是否太過于滯后:
- 如果估算的時間加上最大滯后時間小于本地時間,說明消息太過滯后,需要將
estimate_us
調整為本地時間減去最大滯后時間,并更新link_offset_usec
。
- 如果估算的時間加上最大滯后時間小于本地時間,說明消息太過滯后,需要將
-
最小樣本處理:
- 用于記錄和更新傳輸延遲的最小樣本,逐步收斂傳輸延遲的估算值。
- 如果樣本計數器為 0,初始化最小樣本
min_sample_us
。 - 增加樣本計數器
min_sample_counter
。 - 如果當前差異
diff_us
小于記錄的最小樣本min_sample_us
,則更新最小樣本。 - 如果樣本計數器達到收斂循環次數
convergence_loops
,則更新link_offset_usec
為最小樣本,并重置樣本計數器。
-
返回校正后的時間:
- 最后返回校正后的時間
estimate_us
。
- 最后返回校正后的時間
通過這種樹形結構,可以清晰地看到函數的每個步驟和邏輯分支的關系,更加容易理解整個函數的工作流程。
4. 總結
GPS下的跟隨模式,主要是通過AP_Follow
和ModeFollow
來實現:
AP_Follow
更新跟隨目標的狀態信息ModeFollow
定期根據跟隨目標情況,更新自身的方向和速度,根據兩者之間的距離進行判斷
注:詳細代碼可以仔細閱讀,但是從整個設計邏輯的角度看,其實還是可以理解的。另外,也有一些關于加速度的問題,截止發稿日,還并沒有很好的處理,這個可能和軌跡預測,跟隨車輛指向變化方向改變有關系,對于攝像頭跟隨是很有意義和價值的。
5. 參考資料
【1】ArduPilot開源飛控系統之簡單介紹
【2】ArduPilot之開源代碼框架
【3】ArduPilot開源飛控之飛行模式