Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmake/CustomOptions.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ option(QGC_ENABLE_GST_VIDEOSTREAMING "Enable GStreamer video backend" ON)
# ============================================================================

set(QGC_MAVLINK_GIT_REPO "https://github.com/mavlink/mavlink.git" CACHE STRING "MAVLink repository URL")
set(QGC_MAVLINK_GIT_TAG "c409cf690454db6d3e004bd14173bc6c7ff1e0ff" CACHE STRING "MAVLink repository commit/tag")
set(QGC_MAVLINK_GIT_TAG "1fe1417edba14c178a20303c6914d3b301097a02" CACHE STRING "MAVLink repository commit/tag")
set(QGC_MAVLINK_DIALECT "all" CACHE STRING "MAVLink dialect")
set(QGC_MAVLINK_VERSION "2.0" CACHE STRING "MAVLink protocol version")

Expand Down
7 changes: 7 additions & 0 deletions src/AutoPilotPlugins/PX4/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ target_sources(${CMAKE_PROJECT_NAME}
AirframeComponentAirframes.h
AirframeComponentController.cc
AirframeComponentController.h
FailureInjection.cc
FailureInjection.h
FailureInjectionComponent.cc
FailureInjectionComponent.h
FlightModesComponent.cc
FlightModesComponent.h
PowerComponent.cc
Expand Down Expand Up @@ -108,6 +112,8 @@ qt_add_qml_module(AutoPilotPluginsPX4Module
CalcAmpsPerVoltDialog.qml
CalcVoltageDividerDialog.qml
ESCCalibrationDialog.qml
FailureInjectionComponent.qml
FailureInjectionInstances.js
FlightModesComponentSummary.qml
PowerComponentSummary.qml
PX4FlightBehaviorCopter.qml
Expand Down Expand Up @@ -182,4 +188,5 @@ qt_add_resources(${CMAKE_PROJECT_NAME} autopilot_plugin_px4_qmlimages
"${CMAKE_SOURCE_DIR}/src/AutoPilotPlugins/PX4/Images/VehicleTailDownRotate.png"
"${CMAKE_SOURCE_DIR}/src/AutoPilotPlugins/PX4/Images/VehicleUpsideDown.png"
"${CMAKE_SOURCE_DIR}/src/AutoPilotPlugins/PX4/Images/VehicleUpsideDownRotate.png"
"${CMAKE_SOURCE_DIR}/src/AutoPilotPlugins/PX4/Images/WarningEmergency.svg"
)
203 changes: 203 additions & 0 deletions src/AutoPilotPlugins/PX4/FailureInjection.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
/****************************************************************************
* FailureInjection.cc
****************************************************************************/
#include "FailureInjection.h"

#include <QtCore/QMetaEnum>
#include <QtCore/QVariantMap>

#include "MAVLinkEnumsQml.h" // MAVLinkEnums::FAILURE_UNIT / FAILURE_TYPE, Q_ENUM_NS-reflected from the MAVLink dialect
#include "MAVLinkLib.h" // MAV_RESULT_* for resolveResult()
#include "QGCMAVLink.h" // QGCMAVLink::mavResultToString() fallback for resolveResult()

namespace {

QVariantMap _makeUnit(const QString& name, int unit)
{
return QVariantMap{{"name", name}, {"unit", unit}};
}

QVariantMap _makeType(const QString& name, int type)
{
return QVariantMap{{"name", name}, {"type", type}};
}

/// Strip the longest matching MAVLink enum prefix, e.g. FAILURE_UNIT_SENSOR_GYRO -> GYRO.
QString _stripPrefix(const QString& key, const QStringList& prefixesLongestFirst)
{
for (const QString& prefix : prefixesLongestFirst) {
if (key.startsWith(prefix)) {
return key.mid(prefix.length());
}
}
return key;
}

/// Builds a {name, value} catalog from the Q_ENUM_NS-exposed enum on MAVLinkEnums::staticMetaObject,
/// looked up by name.
QVariantList _buildCatalog(const char* enumName, const QStringList& prefixesLongestFirst,
QVariantMap (*makeEntry)(const QString&, int))
{
QVariantList list;
const QMetaObject& enumsMetaObject = MAVLinkEnums::staticMetaObject;
const QMetaEnum me = enumsMetaObject.enumerator(enumsMetaObject.indexOfEnumerator(enumName));
for (int i = 0; i < me.keyCount(); ++i) {
const QString key = QString::fromLatin1(me.key(i));
if (key.endsWith(QStringLiteral("_ENUM_END"))) {
continue; // dialect sentinel, not a real value
}
list.append(makeEntry(_stripPrefix(key, prefixesLongestFirst), me.value(i)));
}
return list;
}

} // namespace

FailureInjection::FailureInjection(QObject* parent) : QObject(parent)
{
_units = _buildCatalog("FAILURE_UNIT",
{QStringLiteral("FAILURE_UNIT_SENSOR_"), QStringLiteral("FAILURE_UNIT_SYSTEM_"),
QStringLiteral("FAILURE_UNIT_")},
_makeUnit);
_types = _buildCatalog("FAILURE_TYPE", {QStringLiteral("FAILURE_TYPE_")}, _makeType);
}

void FailureInjection::logRow(const QString& unitName, const QString& typeName, const QString& instanceLabel,
const QString& time)
{
_activity.prepend(QVariantMap{{"time", time},
{"unitName", unitName},
{"typeName", typeName},
{"instance", instanceLabel},
{"result", QStringLiteral("pending")}});
emit activityChanged();
}

void FailureInjection::logInjection(const QString& unitName, const QString& typeName, int unitEnum,
const QString& instanceLabel, const QString& time)
{
logRow(unitName, typeName, instanceLabel, time);
if (!_injectedUnits.contains(unitEnum)) {
_injectedUnits.append(unitEnum);
}
}

void FailureInjection::resolveResult(int ackResult)
{
// ACKs for MAV_CMD_INJECT_FAILURE arrive in send order; resolve the oldest pending row (highest index, since newest
// is prepended).
int pendingIndex = -1;
for (int i = _activity.size() - 1; i >= 0; --i) {
if (_activity.at(i).toMap().value(QStringLiteral("result")).toString() == QStringLiteral("pending")) {
pendingIndex = i;
break;
}
}
if (pendingIndex < 0) {
return;
}

if (ackResult == MAV_RESULT_IN_PROGRESS) {
return; // not a terminal result; leave pending
}

QString reason;
switch (ackResult) {
// Not tr()'d: FailureInjectionComponent.qml matches this exact literal to style the row green.
case MAV_RESULT_ACCEPTED:
reason = QStringLiteral("accepted");
break;
case MAV_RESULT_TEMPORARILY_REJECTED:
reason = tr("Temporarily rejected");
break;
case MAV_RESULT_DENIED:
reason = tr("Denied");
break;
case MAV_RESULT_UNSUPPORTED:
reason = tr("Unsupported");
break;
case MAV_RESULT_FAILED:
reason = tr("Failed");
break;
case MAV_RESULT_CANCELLED:
reason = tr("Cancelled");
break;
default:
reason = QGCMAVLink::mavResultToString(static_cast<uint8_t>(ackResult));
break;
}

QVariantMap row = _activity.at(pendingIndex).toMap();
row[QStringLiteral("result")] = reason;
_activity[pendingIndex] = row;
emit activityChanged();
}

QVariantList FailureInjection::injectedUnits(void) const
{
QVariantList list;
for (int unitEnum : _injectedUnits) {
list.append(unitEnum);
}
return list;
}

void FailureInjection::markUnitReset(int unitEnum)
{
_injectedUnits.removeAll(unitEnum);
}

void FailureInjection::resolvePendingInterrupted(void)
{
bool changed = false;
for (int i = 0; i < _activity.size(); ++i) {
QVariantMap row = _activity.at(i).toMap();
if (row.value(QStringLiteral("result")).toString() == QStringLiteral("pending")) {
row[QStringLiteral("result")] = tr("Interrupted");
_activity[i] = row;
changed = true;
}
}
if (changed) {
emit activityChanged();
}
}

void FailureInjection::notifyActiveVehicle(int vehicleId)
{
if ((vehicleId < 0) || (vehicleId == _currentVehicleId)) {
// No vehicle / transient disconnect, or the same vehicle (e.g. after a reboot) — keep the session.
return;
}
_currentVehicleId = vehicleId;
_injectedUnits.clear();
_activity.clear();
emit activityChanged();
}

QVariantList FailureInjection::detailParams(int unitEnum, int typeEnum) const
{
// Vehicle parameters that refine how a failure manifests, keyed by (unit, type). The page shows
// an editor per entry, but only when the connected vehicle actually exposes the parameter.
// Adding a new combo is one more table line.
struct DetailParam
{
int unitEnum;
int typeEnum;
const char* param;
const char* label;
};

static const DetailParam table[] = {
{FAILURE_UNIT_SENSOR_GPS, FAILURE_TYPE_WRONG, "SYS_FAIL_GPS_WRG", QT_TR_NOOP("GPS fix type")},
{FAILURE_UNIT_SYSTEM_BATTERY, FAILURE_TYPE_WRONG, "SYS_FAIL_BAT_LVL", QT_TR_NOOP("Battery level")},
};

QVariantList list;
for (const DetailParam& entry : table) {
if (entry.unitEnum == unitEnum && entry.typeEnum == typeEnum) {
list.append(QVariantMap{{"param", QString::fromLatin1(entry.param)}, {"label", tr(entry.label)}});
}
}
return list;
}
72 changes: 72 additions & 0 deletions src/AutoPilotPlugins/PX4/FailureInjection.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/****************************************************************************
* FailureInjection.h
*
* QML singleton backing the Failure Injection page. It serves two roles:
*
* 1. Static catalog: the MAVLink FAILURE_UNIT / FAILURE_TYPE enums. Values and
* names come from the MAVLink dialect headers (tracking common.xml); the .cc
* derives each display name by stripping the enum-name prefix.
*
* 2. Session state: the activity log and the set of units injected this
* session, persisted here so they survive navigating away from and back
* to the page, which destroys/recreates the page via its SetupPage Loader.
* notifyActiveVehicle() clears the session when the active vehicle changes.
****************************************************************************/
#pragma once

#include <QtCore/QObject>
#include <QtCore/QVariantList>
#include <QtQmlIntegration/QtQmlIntegration>

class FailureInjection : public QObject
{
Q_OBJECT
QML_NAMED_ELEMENT(FailureInjection)
QML_SINGLETON
Q_PROPERTY(QVariantList units READ units CONSTANT) ///< [{ name, unit }]
Q_PROPERTY(QVariantList types READ types CONSTANT) ///< [{ name, type }]
Q_PROPERTY(QVariantList activity READ activity NOTIFY
activityChanged) ///< newest first: [{ time, unitName, typeName, instance, result }]

public:
explicit FailureInjection(QObject* parent = nullptr);

QVariantList units(void) const { return _units; }

QVariantList types(void) const { return _types; }

QVariantList activity(void) const { return _activity; }

/// Add a log row (prepended, newest first, result "pending") without tracking the unit for Reset.
/// instanceLabel is a display descriptor for the affected instance(s), e.g. "all", "2", "1, 3, 5".
Q_INVOKABLE void logRow(const QString& unitName, const QString& typeName, const QString& instanceLabel,
const QString& time);
/// Record one injected failure: adds a log row and remembers the unit so Reset can restore it.
Q_INVOKABLE void logInjection(const QString& unitName, const QString& typeName, int unitEnum,
const QString& instanceLabel, const QString& time);
/// Resolve the oldest still-pending injection with a MAV_RESULT ack code; sets the row result.
Q_INVOKABLE void resolveResult(int ackResult);
/// Distinct FAILURE_UNIT values injected this session, so Reset restores only those.
Q_INVOKABLE QVariantList injectedUnits(void) const;
/// Untrack one FAILURE_UNIT once its reset is accepted, so an interrupted Reset all keeps the rest retryable.
Q_INVOKABLE void markUnitReset(int unitEnum);
/// Resolve any still-"pending" rows to "Interrupted"; called on page (re)load to clear stragglers whose
/// ack was lost when the previous page instance was destroyed mid-send.
Q_INVOKABLE void resolvePendingInterrupted(void);
/// Note the active vehicle's system id; a switch to a different vehicle clears the session so Reset all
/// can't target the wrong vehicle. A negative id (transient disconnect/reboot) is ignored.
Q_INVOKABLE void notifyActiveVehicle(int vehicleId);
/// Vehicle parameters that refine how a (unit, type) failure manifests, e.g. BATTERY+WRONG ->
/// SYS_FAIL_BAT_LVL. Returns [{ param, label }]; empty when the combo has no detail parameters.
Q_INVOKABLE QVariantList detailParams(int unitEnum, int typeEnum) const;

signals:
void activityChanged(void);

private:
QVariantList _units;
QVariantList _types;
QVariantList _activity;
QList<int> _injectedUnits;
int _currentVehicleId = -1; ///< MAVLink system id the session belongs to; -1 until the first vehicle is known
};
34 changes: 34 additions & 0 deletions src/AutoPilotPlugins/PX4/FailureInjectionComponent.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/****************************************************************************
* FailureInjectionComponent.cc
****************************************************************************/
#include "FailureInjectionComponent.h"

#include "AutoPilotPlugin.h"

FailureInjectionComponent::FailureInjectionComponent(Vehicle* vehicle, AutoPilotPlugin* autopilot, QObject* parent)
: VehicleComponent(vehicle, autopilot, AutoPilotPlugin::UnknownVehicleComponent, parent),
_name(tr("Failure Injection"))
{}

QString FailureInjectionComponent::name(void) const
{
return _name;
}

QString FailureInjectionComponent::description(void) const
{
return tr(
"Failure Injection is used to simulate sensor and system failures (MAV_CMD_INJECT_FAILURE) "
"to validate failsafes. Requires SYS_FAILURE_EN = 1 and a vehicle reboot.");
}

QString FailureInjectionComponent::iconResource(void) const
{
return QStringLiteral("/qmlimages/WarningEmergency.svg");
}

QUrl FailureInjectionComponent::setupSource(void) const
{
return QUrl::fromUserInput(
QStringLiteral("qrc:/qml/QGroundControl/AutoPilotPlugins/PX4/FailureInjectionComponent.qml"));
}
37 changes: 37 additions & 0 deletions src/AutoPilotPlugins/PX4/FailureInjectionComponent.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/****************************************************************************
* FailureInjectionComponent.h
* Vehicle Setup component wrapper for the Failure Injection QML page.
****************************************************************************/
#pragma once

#include "VehicleComponent.h"

class FailureInjectionComponent : public VehicleComponent
{
Q_OBJECT
public:
FailureInjectionComponent(Vehicle* vehicle, AutoPilotPlugin* autopilot, QObject* parent = nullptr);

QString name(void) const override;
QString description(void) const override;
QString iconResource(void) const override;

bool requiresSetup(void) const override { return false; }

bool setupComplete(void) const override { return true; }

QStringList setupCompleteChangedTriggerList(void) const override { return QStringList(); }

QUrl setupSource(void) const override;

QUrl summaryQmlSource(void) const override { return QUrl(); }

// Failure injection is meant to be exercised in flight (validate failsafes/EKF), so keep the page usable while
// armed.
bool allowSetupWhileArmed(void) const override { return true; }

bool allowSetupWhileFlying(void) const override { return true; }

private:
const QString _name;
};
Loading
Loading