mirror of
https://github.com/OpenSolo/OpenSolo.git
synced 2025-04-29 14:14:30 +02:00
1025 lines
31 KiB
C++
1025 lines
31 KiB
C++
#include "flightmanager.h"
|
|
#include "buttonmanager.h"
|
|
#include "lockout.h"
|
|
#include "manualoverride.h"
|
|
#include "haptic.h"
|
|
#include "ui.h"
|
|
#include "mavlink.h"
|
|
#include "sologimbal.h"
|
|
|
|
const FlightManager::BatteryState FlightManager::batteryStates[] = {
|
|
{ BatteryNormal, Event::None, BatteryLevelLow, BatteryLevelMax},
|
|
{ BatteryLow, Event::FlightBatteryLow, BatteryLevelCritical, BatteryLevelLowDismiss},
|
|
{ BatteryCritical, Event::FlightBatteryCritical, BatteryLevelFailsafe, BatteryLevelCritical + BatteryLevelDismissBuffer},
|
|
{ BatteryFailsafe, Event::FlightBatteryFailsafe, BatteryLevelMin, BatteryLevelFailsafe + BatteryLevelDismissBuffer},
|
|
};
|
|
|
|
const uint8_t FlightManager::numBattStates = arraysize(FlightManager::batteryStates);
|
|
|
|
FlightManager FlightManager::instance;
|
|
|
|
FlightManager::FlightManager() :
|
|
mode(STABILIZE),
|
|
systemStatus(MAV_STATE_UNINIT),
|
|
armState(Disarmed),
|
|
modeArmableStatus(0),
|
|
takeoffState(TakeoffNone),
|
|
statusTxt(),
|
|
linkConnCounter(LINK_CONN_DURATION),
|
|
command(),
|
|
telemetryVals(),
|
|
currentLoc(),
|
|
home(),
|
|
goodConnectionToSolo(true),
|
|
flagRCFailsafe(false),
|
|
pendingEkfFlags(Telemetry::EKF_UNINIT),
|
|
currentBatteryPhase(BatteryNormal)
|
|
{
|
|
}
|
|
|
|
void FlightManager::init()
|
|
{
|
|
command.state = Command::Complete;
|
|
telemetryVals.clear();
|
|
}
|
|
|
|
const char *FlightManager::flightModeStr(FlightMode m)
|
|
{
|
|
switch (m) {
|
|
case STABILIZE: return "Stabilize";
|
|
case ACRO: return "Acro";
|
|
case ALT_HOLD: return "Alt Hold";
|
|
case AUTO: return "Auto";
|
|
case GUIDED: return "Guided";
|
|
case LOITER: return "Loiter";
|
|
case RTL: return "Return to Home";
|
|
case CIRCLE: return "Circle";
|
|
case LAND: return "Land";
|
|
case DRIFT: return "Drift";
|
|
case SPORT: return "Sport";
|
|
case FLIP: return "Flip";
|
|
case AUTOTUNE: return "Auto Tune";
|
|
case POSHOLD: return "Pos Hold";
|
|
case BRAKE: return "Brake";
|
|
case THROW: return "Throw";
|
|
case AVOID_ADSB: return "ADS-B AVOID";
|
|
case GUIDED_NOGPS: return "Guided No GPS";
|
|
case SMART_RTL: return "Smart RTL";
|
|
case FLOWHOLD: return "Flow Hold";
|
|
case FOLLOW: return "Follow";
|
|
case ZIGZAG: return "Zig Zag";
|
|
default: return "Unknown";
|
|
}
|
|
}
|
|
|
|
void FlightManager::sysHeartbeat()
|
|
{
|
|
/*
|
|
* called from heartbeat task.
|
|
* Do any work that requires periodic attention.
|
|
*/
|
|
|
|
if (linkIsConnected()) {
|
|
if (++linkConnCounter >= LINK_CONN_DURATION) {
|
|
linkDisconnected();
|
|
}
|
|
}
|
|
}
|
|
|
|
void FlightManager::linkConnected()
|
|
{
|
|
/*
|
|
* for now, skip straight to 'InFlight' mode,
|
|
* so we show telemetry data as soon as we're connected.
|
|
* eventually, we may kick off the param check sequence here.
|
|
*/
|
|
|
|
command.state = Command::Complete;
|
|
Ui::instance.pendEvent(Event::VehicleConnectionChanged);
|
|
}
|
|
|
|
void FlightManager::linkDisconnected()
|
|
{
|
|
Ui::instance.alertManager.dismiss(); // TODO: a bit heavy handed, could categorize alerts (e.g. dismissed by vehicle) and check whether alert needs to be dismissed
|
|
Ui::instance.pendEvent(Event::VehicleConnectionChanged);
|
|
|
|
if (inFlight()) {
|
|
flagRCFailsafe = true;
|
|
if (telemetryVals.hasGpsFix()) {
|
|
Ui::instance.pendEvent(Event::RCFailsafe);
|
|
} else {
|
|
Ui::instance.pendEvent(Event::RCFailsafeNoGPS);
|
|
}
|
|
}
|
|
|
|
takeoffState = TakeoffNone;
|
|
systemStatus = MAV_STATE_UNINIT;
|
|
pendingEkfFlags = Telemetry::EKF_UNINIT;
|
|
telemetryVals.clear();
|
|
|
|
ButtonManager::button(Io::ButtonLoiter).setLedInactive();
|
|
ButtonManager::button(Io::ButtonFly).setLedInactive();
|
|
ButtonManager::button(Io::ButtonRTL).setLedInactive();
|
|
}
|
|
|
|
void FlightManager::resetLinkConnCount(const mavlink_message_t * msg)
|
|
{
|
|
/*
|
|
* Any mavlink msg resets the link count if we're already connected.
|
|
* Otherwise, we require a heartbeat to re-establish connection - this
|
|
* helps ensure we have up to date state info about critical fields like
|
|
* arm state, flight mode, system status, and ekf flags.
|
|
*/
|
|
|
|
if (linkIsConnected()) {
|
|
linkConnCounter = 0;
|
|
} else {
|
|
if (msg->msgid == MAVLINK_MSG_ID_HEARTBEAT) {
|
|
linkConnected();
|
|
linkConnCounter = 0;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
void FlightManager::updateRCFailsafeState(uint8_t sysStatus)
|
|
{
|
|
// don't need to do anything if we're not in RCFailsafe state
|
|
if (!flagRCFailsafe) {
|
|
return;
|
|
}
|
|
|
|
flagRCFailsafe = false;
|
|
|
|
// if the vehicle is still in the air, prompt user to be ready to take control // TODO: Hack: do this only when in RTL or LAND mode since there's currently some discrepancy with what triggers an RC failsafe state
|
|
if (inFlight(sysStatus) && (flightMode() == RTL || flightMode() == LAND)) {
|
|
Ui::instance.pendEvent(Event::RCFailsafeRecovery);
|
|
}
|
|
}
|
|
|
|
bool FlightManager::producePacket(HostProtocol::Packet &p)
|
|
{
|
|
/*
|
|
* Our opportunity to send a message via the host.
|
|
*/
|
|
|
|
if (command.state == Command::Pending) {
|
|
mavlink_message_t msg;
|
|
sendCmd(&msg);
|
|
Mavlink::packetizeMsg(p, &msg);
|
|
command.state = Command::Sent;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void FlightManager::requestFlightModeChange(FlightMode m)
|
|
{
|
|
/*
|
|
* helper to request flight mode change
|
|
*/
|
|
|
|
if (mode != m) {
|
|
command.flightMode = m;
|
|
command.set(Command::SetFlightMode);
|
|
}
|
|
}
|
|
|
|
void FlightManager::requestArmStateChange(ArmState as)
|
|
{
|
|
if (armState != as) {
|
|
command.arm = as;
|
|
command.set(Command::SetArmState);
|
|
}
|
|
}
|
|
|
|
void FlightManager::requestHomeWaypoint()
|
|
{
|
|
/*
|
|
* The vehicle's first waypoint represents its
|
|
* home position, which we use for calculating
|
|
* distance from home.
|
|
*/
|
|
|
|
command.waypoint = 0;
|
|
command.set(Command::GetHomeWaypoint);
|
|
}
|
|
|
|
void FlightManager::onFlyButtonEvt(Button *b, Button::Event evt)
|
|
{
|
|
UNUSED(b);
|
|
|
|
if (!Lockout::isUnlocked()) {
|
|
return;
|
|
}
|
|
|
|
if (!linkIsConnected()) {
|
|
return;
|
|
}
|
|
|
|
// battery too low to try to fly?
|
|
if (!inFlight() && telemetryVals.battLevel < BatteryLevelCritical) {
|
|
return;
|
|
}
|
|
|
|
switch (evt) {
|
|
case Button::Hold:
|
|
command.set(Command::FlyButtonHold);
|
|
Haptic::startPattern(Haptic::SingleShort);
|
|
break;
|
|
|
|
case Button::ClickRelease:
|
|
// if we're on the ground and not yet armed,
|
|
// FLY button click puts us into FLY (Loiter) mode
|
|
command.set(Command::FlyButtonClick);
|
|
Haptic::startPattern(Haptic::SingleMedium);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void FlightManager::onRtlButtonEvt(Button *b, Button::Event evt)
|
|
{
|
|
UNUSED(b);
|
|
|
|
if (!Lockout::isUnlocked()) {
|
|
return;
|
|
}
|
|
|
|
if (!linkIsConnected()) {
|
|
return;
|
|
}
|
|
|
|
if (evt == Button::ClickRelease) {
|
|
if (inFlight()) {
|
|
requestFlightModeChange(RTL);
|
|
|
|
if (!telemetryVals.hasGpsFix()) {
|
|
Ui::instance.pendEvent(Event::RTLWithoutGPS);
|
|
}
|
|
|
|
Haptic::startPattern(Haptic::SingleMedium);
|
|
}
|
|
} else if (evt == Button::HoldRelease || evt == Button::LongHold) {
|
|
if (inFlight()) {
|
|
requestFlightModeChange(SMART_RTL);
|
|
|
|
if (!telemetryVals.hasGpsFix()) {
|
|
Ui::instance.pendEvent(Event::RTLWithoutGPS);
|
|
}
|
|
|
|
Haptic::startPattern(Haptic::SingleShort);
|
|
}
|
|
}
|
|
}
|
|
|
|
void FlightManager::onAButtonEvt(Button *b, Button::Event e)
|
|
{
|
|
UNUSED(b);
|
|
|
|
if (!linkIsConnected()) {
|
|
return;
|
|
}
|
|
|
|
if (btnEventShouldForceDisarm(b, e)) {
|
|
forceDisarm();
|
|
return;
|
|
}
|
|
|
|
if (e == Button::ClickRelease) {
|
|
if (ManualOverride::isEnabled()) {
|
|
requestFlightModeChange(ALT_HOLD);
|
|
}
|
|
}
|
|
}
|
|
|
|
void FlightManager::onBButtonEvt(Button *b, Button::Event e)
|
|
{
|
|
if (btnEventShouldForceDisarm(b, e)) {
|
|
forceDisarm();
|
|
return;
|
|
}
|
|
}
|
|
|
|
void FlightManager::onPowerButtonEvt(Button *b, Button::Event e)
|
|
{
|
|
UNUSED(b);
|
|
UNUSED(e);
|
|
}
|
|
|
|
void FlightManager::onPauseButtonEvt(Button *b, Button::Event e)
|
|
{
|
|
if (btnEventShouldForceDisarm(b, e)) {
|
|
forceDisarm();
|
|
return;
|
|
}
|
|
|
|
if (e == Button::ClickRelease) {
|
|
if (inFlight()) {
|
|
command.set(Command::PauseButtonClick);
|
|
Haptic::startPattern(Haptic::SingleMedium);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool FlightManager::btnEventShouldForceDisarm(Button *b, Button::Event e) const
|
|
{
|
|
/*
|
|
* Force disarm will kill the motors and disarm the vehicle,
|
|
* even when armed and/or in the air.
|
|
*/
|
|
|
|
UNUSED(b);
|
|
|
|
if (!armed()) {
|
|
return false;
|
|
}
|
|
|
|
if (e == Button::Hold) {
|
|
if (ButtonManager::button(Io::ButtonA).isHeld() &&
|
|
ButtonManager::button(Io::ButtonB).isHeld() &&
|
|
ButtonManager::button(Io::ButtonLoiter).isHeld())
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void FlightManager::forceDisarm()
|
|
{
|
|
/*
|
|
* Request a force disarm (this will drop the vehicle from the air),
|
|
* and alert the user via haptic.
|
|
*/
|
|
|
|
requestArmStateChange(DisarmForce);
|
|
Haptic::startPattern(Haptic::SingleLong);
|
|
}
|
|
|
|
void FlightManager::beginTakeoff()
|
|
{
|
|
/*
|
|
* We want to takeoff in Loiter - if not already there, change flight modes.
|
|
* Otherwise, send the takeoff command to begin ascending.
|
|
*/
|
|
|
|
if (mode == LOITER) {
|
|
takeoffState = TakeoffSentTakeoffCmd;
|
|
command.set(Command::Takeoff);
|
|
} else {
|
|
takeoffState = TakeoffSetMode;
|
|
requestFlightModeChange(LOITER);
|
|
}
|
|
}
|
|
|
|
void FlightManager::onPixhawkMavlinkMsg(const mavlink_message_t * msg)
|
|
{
|
|
// any mavlink msg from pixhawk keeps our link alive
|
|
resetLinkConnCount(msg);
|
|
|
|
onSoloMavlinkMsg(msg);
|
|
}
|
|
|
|
void FlightManager::onSoloMavlinkMsg(const mavlink_message_t * msg)
|
|
{
|
|
/*
|
|
* Handle a complete msg that has been received from Solo
|
|
*/
|
|
|
|
switch (msg->msgid) {
|
|
case MAVLINK_MSG_ID_HEARTBEAT:
|
|
ArmState vehicleArmState;
|
|
FlightMode vehicleFlightMode;
|
|
mavlink_heartbeat_t heartbeat;
|
|
|
|
mavlink_msg_heartbeat_decode(msg, &heartbeat);
|
|
|
|
vehicleArmState = (heartbeat.base_mode & MAV_MODE_FLAG_SAFETY_ARMED) ? Armed : Disarmed;
|
|
if (vehicleArmState != armState) {
|
|
onArmStateChanged(vehicleArmState);
|
|
armState = vehicleArmState;
|
|
}
|
|
|
|
vehicleFlightMode = static_cast<FlightMode>(heartbeat.custom_mode);
|
|
if (vehicleFlightMode != mode) {
|
|
onFlightModeChanged(vehicleFlightMode);
|
|
mode = vehicleFlightMode;
|
|
}
|
|
|
|
if (heartbeat.system_status != systemStatus) {
|
|
onSystemStatusChanged(heartbeat.system_status);
|
|
systemStatus = heartbeat.system_status;
|
|
}
|
|
|
|
// see comment in MAVLINK_MSG_ID_EKF_STATUS_REPORT handling
|
|
if (telemetryVals.ekfFlags != pendingEkfFlags) {
|
|
onEkfChanged(pendingEkfFlags);
|
|
telemetryVals.ekfFlags = pendingEkfFlags;
|
|
}
|
|
break;
|
|
|
|
case MAVLINK_MSG_ID_GLOBAL_POSITION_INT: {
|
|
Coord2D c;
|
|
mavlink_global_position_int_t global_pos;
|
|
mavlink_msg_global_position_int_decode(msg, &global_pos);
|
|
// lat and long are degrees * 1E7, so convert back
|
|
c.set(global_pos.lat / 1e7, global_pos.lon / 1e7);
|
|
onGpsPositionChanged(c);
|
|
currentLoc.set(c.lat(), c.lng());
|
|
|
|
// global_position_int uses mm, so convert altitude to meters
|
|
float alt_m = global_pos.relative_alt * 0.001;
|
|
|
|
// Used to determine if takeoff is complete
|
|
if (takeoffState == TakeoffAscending) {
|
|
if (alt_m >= (TakeoffAltitude - 0.2)) {
|
|
takeoffState = TakeoffComplete;
|
|
}
|
|
}
|
|
|
|
// Altitude display on the controller
|
|
// Formerly used vfr_hud, now using global_position_int.relative_alt
|
|
if (!isWithin(telemetryVals.altitude, alt_m, 0.1)) {
|
|
onAltitudeChanged(alt_m);
|
|
telemetryVals.altitude = alt_m;
|
|
}
|
|
} break;
|
|
|
|
case MAVLINK_MSG_ID_GPS_RAW_INT:
|
|
mavlink_gps_raw_int_t gps_raw_int;
|
|
mavlink_msg_gps_raw_int_decode(msg, &gps_raw_int);
|
|
if (telemetryVals.gpsFix != gps_raw_int.fix_type) {
|
|
onGpsFixChanged(gps_raw_int.fix_type);
|
|
telemetryVals.gpsFix = gps_raw_int.fix_type;
|
|
}
|
|
|
|
if (telemetryVals.numSatellites != gps_raw_int.satellites_visible) {
|
|
onGpsNumSatellitesChanged(gps_raw_int.satellites_visible);
|
|
telemetryVals.numSatellites = gps_raw_int.satellites_visible;
|
|
}
|
|
break;
|
|
|
|
case MAVLINK_MSG_ID_VFR_HUD:
|
|
mavlink_vfr_hud_t vfr_hud;
|
|
mavlink_msg_vfr_hud_decode(msg, &vfr_hud);
|
|
telemetryVals.airSpeed = vfr_hud.airspeed;
|
|
telemetryVals.groundSpeed = vfr_hud.groundspeed;
|
|
break;
|
|
|
|
case MAVLINK_MSG_ID_SYS_STATUS:
|
|
int8_t battLevel;
|
|
battLevel = mavlink_msg_sys_status_get_battery_remaining(msg);
|
|
|
|
if (telemetryVals.battLevel != battLevel) {
|
|
onBatteryChanged(battLevel);
|
|
telemetryVals.battLevel = battLevel;
|
|
}
|
|
break;
|
|
|
|
case MAVLINK_MSG_ID_COMMAND_ACK:
|
|
mavlink_command_ack_t ack;
|
|
mavlink_msg_command_ack_decode(msg, &ack);
|
|
if (ack.command == command.id) {
|
|
command.state = Command::Complete; // completion handlers may update this
|
|
}
|
|
cmdAcknowledged(&ack);
|
|
break;
|
|
|
|
case MAVLINK_MSG_ID_STATUSTEXT:
|
|
mavlink_msg_statustext_decode(msg, &statusTxt.mav);
|
|
statusTxt.rxtime = SysTime::now();
|
|
onStatusTextChanged();
|
|
break;
|
|
|
|
// 1-3 Hz arm state update, constant rate depending on current flight code
|
|
case MAVLINK_MSG_ID_NAMED_VALUE_INT:{
|
|
mavlink_named_value_int_t namedVal;
|
|
const char * const maskName = "ARMMASK";
|
|
|
|
mavlink_msg_named_value_int_decode(msg, &namedVal);
|
|
if (strncmp(namedVal.name, maskName, strlen(maskName)) == 0) { // make sure we have the right message type
|
|
if (modeArmableStatus != namedVal.value) { // check to see if status has changed
|
|
onArmableStatusChanged(namedVal.value);
|
|
}
|
|
}
|
|
}break;
|
|
|
|
case MAVLINK_MSG_ID_MOUNT_STATUS:
|
|
mavlink_mount_status_t mountStatus;
|
|
mavlink_msg_mount_status_decode(msg, &mountStatus);
|
|
SoloGimbal::instance.onMountStatus(mountStatus);
|
|
break;
|
|
|
|
case MAVLINK_MSG_ID_RADIO_STATUS:
|
|
int8_t signedRssi;
|
|
mavlink_radio_status_t radioStatus;
|
|
mavlink_msg_radio_status_decode(msg, &radioStatus);
|
|
|
|
/*
|
|
* The mavlink message packs rssi as a uint8_t
|
|
* but SoloLink generates it as an int8_t, typically
|
|
* in the range of -80 to -30.
|
|
*/
|
|
|
|
signedRssi = static_cast<int8_t>(radioStatus.remrssi);
|
|
if (telemetryVals.rssi != signedRssi) {
|
|
onRssiChanged(signedRssi);
|
|
telemetryVals.rssi = signedRssi;
|
|
}
|
|
break;
|
|
|
|
case MAVLINK_MSG_ID_EKF_STATUS_REPORT:
|
|
pendingEkfFlags = mavlink_msg_ekf_status_report_get_flags(msg);
|
|
/*
|
|
* just mark the ekf flags as dirty - we will process
|
|
* them the next time we receive a heartbeat, since
|
|
* the ekf processing depends on an up to date arm state.
|
|
*/
|
|
break;
|
|
|
|
case MAVLINK_MSG_ID_MISSION_ITEM:
|
|
mavlink_mission_item_t item;
|
|
mavlink_msg_mission_item_decode(msg, &item);
|
|
|
|
if (home.update(item)) {
|
|
Ui::instance.pendEvent(Event::HomeLocationChanged);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
void FlightManager::onArmStateChanged(ArmState as)
|
|
{
|
|
if (as == Armed) {
|
|
requestHomeWaypoint();
|
|
|
|
} else if (as == Disarmed) {
|
|
takeoffState = TakeoffNone;
|
|
|
|
// return to loiter if in auto mode
|
|
if (flightModeIsAutonomous(mode)) {
|
|
requestFlightModeChange(LOITER);
|
|
}
|
|
}
|
|
|
|
Ui::instance.pendEvent(Event::ArmStateUpdated);
|
|
|
|
// DBG(("arm status: %d\n", as));
|
|
}
|
|
|
|
void FlightManager::onSystemStatusChanged(uint8_t ss)
|
|
{
|
|
Button & btn = ButtonManager::button(Io::ButtonFly);
|
|
|
|
if (ss == MAV_STATE_ACTIVE) {
|
|
btn.setLedActive();
|
|
} else {
|
|
btn.setLedInactive();
|
|
}
|
|
|
|
updateRCFailsafeState(ss);
|
|
// DBG(("system status: %d\n", ss));
|
|
}
|
|
|
|
void FlightManager::onFlightModeChanged(FlightMode fm)
|
|
{
|
|
ButtonManager::onFlightModeChanged(fm);
|
|
Ui::instance.pendEvent(Event::FlightModeChanged);
|
|
|
|
// DBG(("flightMode: %s\n", flightModeStr(fm)));
|
|
}
|
|
|
|
void FlightManager::onGpsFixChanged(uint8_t gps)
|
|
{
|
|
UNUSED(gps);
|
|
|
|
#if 0
|
|
// Dismiss lockout alert when GPS is regained
|
|
if (Ui::instance.alertManager.currentAlertWaitingForGps() && Telemetry::isGPSLevelFix(gps)){
|
|
Ui::instance.pendEvent(Event::AlertRecovery);
|
|
return;
|
|
}
|
|
|
|
// using EKF to determine GPS availability, rather than reported gps fix
|
|
if (inFlight()) {
|
|
bool prevGpsOk = Telemetry::hasGpsFix();
|
|
bool currGpsOk = Telemetry::isGPSLevelFix(gps);
|
|
|
|
// GPS lost
|
|
if (prevGpsOk && !currGpsOk) {
|
|
if (currentFlightModeManual()) {
|
|
Ui::instance.pendEvent(Event::GpsLostManual);
|
|
} else {
|
|
Ui::instance.pendEvent(Event::GpsLost);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// GPS regained
|
|
if (!prevGpsOk && currGpsOk) {
|
|
Ui::instance.pendEvent(Event::GpsLostRecovery);
|
|
return;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// DBG(("gps fix: %d\n", gps));
|
|
}
|
|
|
|
void FlightManager::onGpsNumSatellitesChanged(uint8_t numsat)
|
|
{
|
|
UNUSED(numsat);
|
|
|
|
Ui::instance.pendEvent(Event::GpsNumSatellitesChanged);
|
|
}
|
|
|
|
void FlightManager::onAltitudeChanged(float alt)
|
|
{
|
|
UNUSED(alt);
|
|
|
|
Ui::instance.pendEvent(Event::AltitudeUpdated);
|
|
}
|
|
|
|
void FlightManager::onBatteryChanged(int8_t battLevel)
|
|
{
|
|
// check if we need to dismiss or bring up new battery alerts
|
|
if (batteryPhaseTransition(battLevel)) {
|
|
updateBatteryPhase(battLevel);
|
|
}
|
|
|
|
Ui::instance.pendEvent(Event::FlightBatteryChanged);
|
|
// DBG(("batt: %d\n", battLevel));
|
|
}
|
|
|
|
bool FlightManager::batteryPhaseTransition(int8_t battLevel)
|
|
{
|
|
if (battLevel == BatteryLevelNotSet) {
|
|
return false; // don't do anything
|
|
}
|
|
|
|
const BatteryState & batteryState = batteryStates[currentBatteryPhase];
|
|
battLevel = clamp(battLevel, BatteryLevelMin, BatteryLevelMax);
|
|
return !(battLevel > batteryState.minLevel && battLevel <= batteryState.maxLevel);
|
|
}
|
|
|
|
void FlightManager::updateBatteryPhase(int8_t battLevel)
|
|
{
|
|
battLevel = clamp(battLevel, BatteryLevelMin, BatteryLevelMax);
|
|
for (uint8_t i=0; i < numBattStates; ++i) {
|
|
const BatteryState & batteryState = batteryStates[i];
|
|
// update battery phase based on the lowest mininum battery level threshold
|
|
if (battLevel > batteryState.minLevel && currentBatteryPhase != batteryState.phase) {
|
|
currentBatteryPhase = batteryState.phase;
|
|
updateBatteryAlert(batteryState.event);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void FlightManager::updateBatteryAlert(Event::ID id)
|
|
{
|
|
// check to see if we need to dismiss any battery alerts
|
|
// note: if battery was messed up and we need to transition from a fullscreen alert
|
|
// to a banner alert, we would need to first dismiss the fullscreen alert
|
|
switch (Ui::instance.alertManager.currentEvent()) {
|
|
case Event::FlightBatteryLow:
|
|
case Event::FlightBatteryFailsafe:
|
|
case Event::FlightBatteryCritical:
|
|
case Event::FlightBatteryTooLowForTakeoff:
|
|
Ui::instance.alertManager.dismiss();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (currentBatteryPhase != BatteryNormal) {
|
|
Ui::instance.pendEvent(id);
|
|
}
|
|
}
|
|
|
|
|
|
void FlightManager::onRssiChanged(int8_t rssi)
|
|
{
|
|
if (UiTelemetry::rssiBars(telemetryVals.rssi) != UiTelemetry::rssiBars(rssi)) {
|
|
Ui::instance.pendEvent(Event::RssiUpdated);
|
|
}
|
|
|
|
#if 0 // TODO: disabled until we have better ways to determine poor connection quality
|
|
if (UiTelemetry::rssiBars(rssi) <= RSSI_BARS_POOR_CONNECTION){
|
|
// Connection to Solo is poor, notify user the first time this occurs
|
|
if (goodConnectionToSolo){
|
|
goodConnectionToSolo = false;
|
|
Ui::instance.pendEvent(Event::SoloConnectionPoor);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// DBG(("rssi: %d\n", rssi));
|
|
}
|
|
|
|
void FlightManager::onGpsPositionChanged(const Coord2D & c)
|
|
{
|
|
UNUSED(c); // not used at the moment
|
|
|
|
Ui::instance.pendEvent(Event::GpsPositionChanged);
|
|
}
|
|
|
|
bool FlightManager::modeIsNotArmable(const char * statusString) const
|
|
{
|
|
const char * const notArmable = "Arm: Mode not armable";
|
|
return (strncmp(statusString, notArmable, strlen(notArmable)) == 0);
|
|
}
|
|
|
|
void FlightManager::processStatusText(const char * statusString)
|
|
{
|
|
const char * const serviceRequiredStrings[] = {
|
|
"PreArm: Accelerometers not healthy",
|
|
"PreArm: Gyros not healthy",
|
|
"PreArm: Compass not healthy",
|
|
"PreArm: Barometer not healthy",
|
|
"PreArm: Barometer not healthy!",
|
|
"PreArm: Check Board Voltage",
|
|
};
|
|
|
|
const char * const levelErrorStrings[] = {
|
|
"PreArm: INS not calibrated",
|
|
"PreArm: inconsistent Gyros",
|
|
"PreArm: inconsistent Accelerometers",
|
|
};
|
|
|
|
const char * const altDisparityStrings[] = {
|
|
"PreArm: Altitude disparity",
|
|
};
|
|
|
|
const char * const waitingForNavChecksStrings[] = {
|
|
"PreArm: Waiting for Nav Checks",
|
|
"Arm: Waiting for Nav Checks",
|
|
};
|
|
|
|
const char * const compassCalStrings[] = {
|
|
"PreArm: Compass not calibrated",
|
|
"PreArm: Compass offsets too high",
|
|
};
|
|
|
|
const char * const compassInterferenceStrings[] = {
|
|
"PreArm: Check mag field",
|
|
"PreArm: inconsistent compasses",
|
|
};
|
|
|
|
const char * const levelCalRunningStrings[] = {
|
|
"Arm: Accelerometer calibration running",
|
|
};
|
|
|
|
const char * const compassCalRunningStrings[] = {
|
|
"Arm: Compass calibration running",
|
|
};
|
|
|
|
const char * const waitingForSoloCalStrings[] = {
|
|
"Arm: Altitude disparity",
|
|
};
|
|
|
|
const char * const calFailedStrings[] = {
|
|
"Arm: Gyro calibration failed",
|
|
};
|
|
|
|
const char * const throttleTooHigh[] = {
|
|
"Arm: Throttle too high",
|
|
};
|
|
|
|
const char * const leaningStrings[] = {
|
|
"Arm: Leaning",
|
|
};
|
|
|
|
struct StatusTextEvent {
|
|
const char * const *strings;
|
|
unsigned count;
|
|
Event::ID event;
|
|
} const statusTextEvents[] = {
|
|
{ serviceRequiredStrings, arraysize(serviceRequiredStrings), Event::VehicleRequiresService },
|
|
{ levelErrorStrings, arraysize(levelErrorStrings), Event::LevelError },
|
|
{ altDisparityStrings, arraysize(altDisparityStrings), Event::AltitudeCalRequired },
|
|
{ waitingForNavChecksStrings, arraysize(waitingForNavChecksStrings), Event::WaitingForNavChecks },
|
|
{ compassCalStrings, arraysize(compassCalStrings), Event::CompassCalRequired },
|
|
{ compassInterferenceStrings, arraysize(compassInterferenceStrings), Event::CompassInterference },
|
|
{ compassCalRunningStrings, arraysize(compassCalRunningStrings), Event::CompassCalibrating },
|
|
{ levelCalRunningStrings, arraysize(levelCalRunningStrings), Event::LevelCalibrating },
|
|
{ waitingForSoloCalStrings, arraysize(waitingForSoloCalStrings), Event::VehicleCalibrating },
|
|
{ calFailedStrings, arraysize(calFailedStrings), Event::CalibrationFailed },
|
|
{ throttleTooHigh, arraysize(throttleTooHigh), Event::ThrottleError },
|
|
{ leaningStrings, arraysize(leaningStrings), Event::CantArmWhileLeaning },
|
|
};
|
|
|
|
// if status text matches one of our strings, post the corresponding event
|
|
for (unsigned i = 0; i < arraysize(statusTextEvents); ++i) {
|
|
const StatusTextEvent & ste = statusTextEvents[i];
|
|
for (unsigned j = 0; j < ste.count; ++j) {
|
|
if (strncmp(statusString, ste.strings[j], strlen(ste.strings[j])) == 0) {
|
|
Ui::instance.pendEvent(ste.event);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool unrecognizedPrearm = (strncmp(statusString,"Arm: ",strlen("Arm: ")) == 0) || (strncmp(statusString,"PreArm: ", strlen("PreArm: ")) == 0);
|
|
// if status text is an unrecognized pre-arm failure,
|
|
if (Ui::instance.alertManager.currentAlertVehiclePreArm() && unrecognizedPrearm) {
|
|
Ui::instance.alertManager.dismiss();
|
|
return;
|
|
}
|
|
}
|
|
|
|
void FlightManager::onStatusTextChanged()
|
|
{
|
|
/*
|
|
* There are several status text messages that correspond to
|
|
* conditions that we think we can't easily recover from.
|
|
*
|
|
* Unfortunately, these are not enumerated anywhere, we just
|
|
* need to strcmp and hope none of the strings get changed in the flight code...
|
|
*/
|
|
|
|
if (modeIsNotArmable(statusTxt.mav.text)) {
|
|
requestFlightModeChange(LOITER);
|
|
return;
|
|
}
|
|
|
|
processStatusText(statusTxt.mav.text);
|
|
}
|
|
|
|
void FlightManager::onArmableStatusChanged(int32_t statusVal)
|
|
{
|
|
modeArmableStatus = statusVal;
|
|
|
|
// check to see if we need to dismiss any PreArm alerts, want to be sure we don't dismiss any non-PreArm alerts (e.g. battery warnings, etc.)
|
|
if (readyToArmWithoutGPS() && Ui::instance.alertManager.currentAlertVehiclePreArm()) {
|
|
Ui::instance.alertManager.dismiss();
|
|
}
|
|
}
|
|
|
|
void FlightManager::onEkfChanged(uint16_t ekfFlags)
|
|
{
|
|
/*
|
|
* if current gps is ok, and transitioning to not ok, notify ui.
|
|
*
|
|
* XXX: also probably want to request home location again here,
|
|
* if we've gained gps mid-flight, since it may have been updated.
|
|
* Not doing this for now, since we need to figure out our
|
|
* strategy for communicating to the user that when they press
|
|
* RTL, the vehicle may not actually come back to them...
|
|
*/
|
|
|
|
Ui::instance.pendEvent(Event::GpsFixChanged);
|
|
|
|
// Pend alert if GPS is lost or regained in flight.
|
|
// these checks require a known ekf state, so do not compute
|
|
// if we don't have up to date ekf state.
|
|
if (inFlight() && telemetryVals.ekfFlagsInitialized()) {
|
|
bool prevGpsOk = Telemetry::isEkfGpsOk(telemetryVals.ekfFlags);
|
|
bool currGpsOk = Telemetry::isEkfGpsOk(ekfFlags);
|
|
|
|
// GPS lost
|
|
if (prevGpsOk && !currGpsOk) {
|
|
if (currentFlightModeManual()) {
|
|
Ui::instance.pendEvent(Event::GpsLostManual);
|
|
} else {
|
|
Ui::instance.pendEvent(Event::GpsLost);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// GPS regained
|
|
if (!prevGpsOk && currGpsOk) {
|
|
Ui::instance.pendEvent(Event::GpsLostRecovery);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// DBG(("ekfFlags change: 0x%x\n", ekfFlags));
|
|
}
|
|
|
|
void FlightManager::sendCmd(mavlink_message_t *msg)
|
|
{
|
|
/*
|
|
* Produce a mavlink packet for the currently pending command.
|
|
* Called when we have a command ready to be sent to the vehicle.
|
|
*/
|
|
|
|
switch (command.id) {
|
|
case Command::SetArmState:
|
|
mavlink_msg_command_long_pack(Mavlink::ArtooSysID, Mavlink::ArtooComponentID, msg,
|
|
Mavlink::SoloSysID, MAV_COMP_ID_SYSTEM_CONTROL,
|
|
MAV_CMD_COMPONENT_ARM_DISARM,
|
|
0, // confirmation
|
|
(command.arm == Armed) ? 1 : 0, // 1 for arm, 0 for disarm
|
|
(command.arm == DisarmForce) ? ForceDisarmMagic : 0,
|
|
0, 0, 0, 0, 0); // params 3-7
|
|
break;
|
|
|
|
case Command::Takeoff:
|
|
mavlink_msg_command_long_pack(Mavlink::ArtooSysID, Mavlink::ArtooComponentID, msg,
|
|
Mavlink::SoloSysID, 1,
|
|
MAV_CMD_NAV_TAKEOFF,
|
|
0, // confirmation
|
|
0, 0, 0, 0, 0, 0, // params 1-6
|
|
TakeoffAltitude); // param 7 - altitude in meters
|
|
break;
|
|
|
|
case Command::SetFlightMode:
|
|
mavlink_msg_set_mode_pack(Mavlink::ArtooSysID, Mavlink::ArtooComponentID, msg, Mavlink::SoloSysID,
|
|
MAV_MODE_FLAG_CUSTOM_MODE_ENABLED, command.flightMode);
|
|
break;
|
|
|
|
case Command::FlyButtonClick:
|
|
mavlink_msg_command_long_pack(Mavlink::ArtooSysID, Mavlink::ArtooComponentID, msg,
|
|
Mavlink::SoloSysID, MAV_COMP_ID_SYSTEM_CONTROL,
|
|
MAV_CMD_SOLO_BTN_FLY_CLICK,
|
|
0, // confirmation
|
|
0, 0, 0, 0, 0, 0, 0); // params 1-7
|
|
break;
|
|
|
|
case Command::FlyButtonHold:
|
|
mavlink_msg_command_long_pack(Mavlink::ArtooSysID, Mavlink::ArtooComponentID, msg,
|
|
Mavlink::SoloSysID, MAV_COMP_ID_SYSTEM_CONTROL,
|
|
MAV_CMD_SOLO_BTN_FLY_HOLD,
|
|
0, // confirmation
|
|
TakeoffAltitude,
|
|
0, 0, 0, 0, 0, 0); // params 2-7
|
|
break;
|
|
|
|
case Command::PauseButtonClick:
|
|
mavlink_msg_command_long_pack(Mavlink::ArtooSysID, Mavlink::ArtooComponentID, msg,
|
|
Mavlink::SoloSysID, MAV_COMP_ID_SYSTEM_CONTROL,
|
|
MAV_CMD_SOLO_BTN_PAUSE_CLICK,
|
|
0, // confirmation
|
|
0, // param 1: inShot not used yet
|
|
0, 0, 0, 0, 0, 0); // params 2-7
|
|
break;
|
|
|
|
case Command::GetHomeWaypoint:
|
|
mavlink_msg_mission_request_pack(Mavlink::ArtooSysID, Mavlink::ArtooComponentID, msg,
|
|
Mavlink::SoloSysID, Mavlink::SoloComponentID,
|
|
command.waypoint);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void FlightManager::cmdAcknowledged(const mavlink_command_ack_t *ack)
|
|
{
|
|
/*
|
|
* A command has been acked by the vehicle.
|
|
* Tick along to our next state as appropriate.
|
|
*/
|
|
|
|
switch (ack->command) {
|
|
case MAV_CMD_COMPONENT_ARM_DISARM:
|
|
if (ack->result != MAV_RESULT_ACCEPTED) {
|
|
Ui::instance.arming.onArmFailed();
|
|
}
|
|
break;
|
|
|
|
case MAVLINK_MSG_ID_SET_MODE:
|
|
onFlightModeAck(ack);
|
|
break;
|
|
|
|
case MAV_CMD_NAV_TAKEOFF:
|
|
if (ack->result == MAV_RESULT_ACCEPTED) {
|
|
takeoffState = TakeoffAscending;
|
|
} else {
|
|
Ui::instance.arming.onTakeoffFailed();
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (ack->result != MAV_RESULT_ACCEPTED) {
|
|
DBG(("mavlink nack. cmd %d, result %d\n", ack->command, ack->result));
|
|
}
|
|
}
|
|
|
|
void FlightManager::onFlightModeAck(const mavlink_command_ack_t *ack)
|
|
{
|
|
/*
|
|
* A flight mode request has been acknowledged.
|
|
*
|
|
* Mainly, we're checking to see whether the mode change was a
|
|
* change to LOITER to enable takeoff.
|
|
*/
|
|
|
|
if (ack->result == MAV_RESULT_ACCEPTED) {
|
|
if (takeoffState == TakeoffSetMode && systemStatus == MAV_STATE_STANDBY) {
|
|
takeoffState = TakeoffSentTakeoffCmd;
|
|
command.set(Command::Takeoff);
|
|
return;
|
|
}
|
|
}
|
|
|
|
takeoffState = TakeoffNone;
|
|
}
|