diff --git a/docs/en/qgc-user-guide/plan_view/plan_view.md b/docs/en/qgc-user-guide/plan_view/plan_view.md index a41dc37f197..b782c75ba3b 100644 --- a/docs/en/qgc-user-guide/plan_view/plan_view.md +++ b/docs/en/qgc-user-guide/plan_view/plan_view.md @@ -54,6 +54,9 @@ The plan tools are a vertical tool strip on the left side of the map, used for a Inserts a takeoff command into the mission. This tool is available for all vehicle types except rovers. +For PX4 VTOL vehicles, choose **VTOL takeoff** to climb and transition to fixed-wing flight, or +**Multicopter takeoff** to remain in multicopter mode. The VTOL takeoff remains the default choice. + ### Pattern The [Pattern](pattern.md) tool simplifies the creation of missions for flying complex geometries, including [surveys](../plan_view/pattern_survey.md) and [structure scans](../plan_view/pattern_structure_scan_v2.md). diff --git a/src/FirmwarePlugin/FirmwarePlugin.h b/src/FirmwarePlugin/FirmwarePlugin.h index 3de580e6631..f05307fe206 100644 --- a/src/FirmwarePlugin/FirmwarePlugin.h +++ b/src/FirmwarePlugin/FirmwarePlugin.h @@ -87,6 +87,7 @@ class FirmwarePlugin : public QObject ROIModeCapability = 1 << 5, ///< Vehicle supports ROI (both in Fly guided mode and from Plan creation) ChangeHeadingCapability = 1 << 6, ///< Vehicle supports changing heading at current location GuidedTakeoffCapability = 1 << 7, ///< Vehicle supports guided takeoff + VTOLMulticopterTakeoffCapability = 1 << 8, ///< VTOL supports MAV_CMD_NAV_TAKEOFF while remaining in multicopter mode }; /// Parameter name remapping support: diff --git a/src/FirmwarePlugin/PX4/PX4-MavCmdInfoVTOL.json b/src/FirmwarePlugin/PX4/PX4-MavCmdInfoVTOL.json index e03df84d9b8..163a3f3fcdf 100644 --- a/src/FirmwarePlugin/PX4/PX4-MavCmdInfoVTOL.json +++ b/src/FirmwarePlugin/PX4/PX4-MavCmdInfoVTOL.json @@ -4,5 +4,10 @@ "fileType": "MavCmdInfo", "mavCmdInfo": [ + { + "id": 22, + "comment": "MAV_CMD_NAV_TAKEOFF", + "description": "Take off vertically and continue the mission in multicopter mode." + } ] } diff --git a/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.cc b/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.cc index ad9f0bbd9e1..8e458815988 100644 --- a/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.cc +++ b/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.cc @@ -174,6 +174,9 @@ bool PX4FirmwarePlugin::isCapable(const Vehicle *vehicle, FirmwareCapabilities c if (vehicle->multiRotor() || vehicle->vtol()) { available |= TakeoffVehicleCapability | GuidedTakeoffCapability | OrbitModeCapability; } + if (vehicle->vtol()) { + available |= VTOLMulticopterTakeoffCapability; + } if (vehicle->fixedWing()) { available |= TakeoffVehicleCapability | OrbitModeCapability; } diff --git a/src/MissionManager/MissionController.cc b/src/MissionManager/MissionController.cc index 9326262fca2..fbdd39b08b1 100644 --- a/src/MissionManager/MissionController.cc +++ b/src/MissionManager/MissionController.cc @@ -24,6 +24,7 @@ #include "TakeoffMissionItem.h" #include "PlanViewSettings.h" #include "MissionCommandTree.h" +#include "AppMessages.h" #include "QGCMath.h" #include "QGCLoggingCategory.h" @@ -71,7 +72,7 @@ MissionController::~MissionController() void MissionController::_resetMissionFlightStatus(void) { - _flightStatusCalc.reset(_controllerVehicle, _managerVehicle, _missionContainsVTOLTakeoff); + _flightStatusCalc.reset(_controllerVehicle, _managerVehicle, _missionStartsInVTOLMulticopterMode); _missionFlightStatus = _flightStatusCalc.status(); emit missionPlannedDistanceChanged(_missionFlightStatus.plannedDistance); @@ -313,9 +314,33 @@ VisualMissionItem* MissionController::insertSimpleMissionItem(QGeoCoordinate coo } VisualMissionItem* MissionController::insertTakeoffItem(QGeoCoordinate /*coordinate*/, int visualItemIndex, bool makeCurrentItem) +{ + return _insertTakeoffItemWorker( + _controllerVehicle->vtol() ? MAV_CMD_NAV_VTOL_TAKEOFF : MAV_CMD_NAV_TAKEOFF, + visualItemIndex, + makeCurrentItem); +} + +VisualMissionItem* MissionController::insertVTOLMulticopterTakeoffItem( + QGeoCoordinate /*coordinate*/, int visualItemIndex, bool makeCurrentItem) +{ + const FirmwarePlugin* firmwarePlugin = _controllerVehicle->firmwarePlugin(); + const QList supportedCommands = firmwarePlugin->supportedMissionCommands(_controllerVehicle->vehicleClass()); + const bool commandSupported = supportedCommands.isEmpty() || supportedCommands.contains(MAV_CMD_NAV_TAKEOFF); + if (!_controllerVehicle->vtol() + || !firmwarePlugin->isCapable(_controllerVehicle, FirmwarePlugin::VTOLMulticopterTakeoffCapability) + || !commandSupported) { + QGC::showAppMessage(tr("Multicopter takeoff is not supported for this vehicle and firmware.")); + return nullptr; + } + + return _insertTakeoffItemWorker(MAV_CMD_NAV_TAKEOFF, visualItemIndex, makeCurrentItem); +} + +VisualMissionItem* MissionController::_insertTakeoffItemWorker(MAV_CMD command, int visualItemIndex, bool makeCurrentItem) { int sequenceNumber = _nextSequenceNumber(); - _takeoffMissionItem = new TakeoffMissionItem(_controllerVehicle->vtol() ? MAV_CMD_NAV_VTOL_TAKEOFF : MAV_CMD_NAV_TAKEOFF, _masterController, _flyView, _settingsItem, false /* forLoad */); + _takeoffMissionItem = new TakeoffMissionItem(command, _masterController, _flyView, _settingsItem, false /* forLoad */); _takeoffMissionItem->setSequenceNumber(sequenceNumber); _initVisualItem(_takeoffMissionItem); @@ -996,7 +1021,7 @@ void MissionController::_recalcFlightPathSegments(void) FlightPathSegmentHashTable oldSegmentTable = _flightPathSegmentHashTable; - _missionContainsVTOLTakeoff = false; + _missionStartsInVTOLMulticopterMode = false; _flightPathSegmentHashTable.clear(); _simpleFlightPathSegments.beginResetModel(); @@ -1036,14 +1061,19 @@ void MissionController::_recalcFlightPathSegments(void) MAV_CMD command = simpleItem->mavCommand(); switch (command) { case MAV_CMD_NAV_TAKEOFF: + if (firstCoordinateNotFound + && _controllerVehicle->firmwarePlugin()->isCapable( + _controllerVehicle, FirmwarePlugin::VTOLMulticopterTakeoffCapability)) { + _missionStartsInVTOLMulticopterMode = true; + } + if (!linkEndToHome && firstCoordinateNotFound) { + linkStartToHome = true; + } + break; case MAV_CMD_NAV_VTOL_TAKEOFF: - _missionContainsVTOLTakeoff = command == MAV_CMD_NAV_VTOL_TAKEOFF; - if (!linkEndToHome) { - // If we still haven't found the first coordinate item and we hit a takeoff command this means the mission starts from the ground. - // Link the first item back to home to show that. - if (firstCoordinateNotFound) { - linkStartToHome = true; - } + if (!linkEndToHome && firstCoordinateNotFound) { + _missionStartsInVTOLMulticopterMode = true; + linkStartToHome = true; } break; case MAV_CMD_NAV_RETURN_TO_LAUNCH: @@ -1173,7 +1203,9 @@ void MissionController::_recalcMissionFlightStatus() qCDebug(MissionControllerLog) << "_recalcMissionFlightStatus"; - _flightStatusCalc.recalc(_visualItems, _settingsItem, _controllerVehicle, _managerVehicle, _appSettings, _planViewSettings, _missionContainsVTOLTakeoff); + _flightStatusCalc.recalc( + _visualItems, _settingsItem, _controllerVehicle, _managerVehicle, _appSettings, _planViewSettings, + _missionStartsInVTOLMulticopterMode); _missionFlightStatus = _flightStatusCalc.status(); _minAMSLAltitude = _flightStatusCalc.minAMSLAltitude(); _maxAMSLAltitude = _flightStatusCalc.maxAMSLAltitude(); diff --git a/src/MissionManager/MissionController.h b/src/MissionManager/MissionController.h index 61ad9a65825..84a27bde99d 100644 --- a/src/MissionManager/MissionController.h +++ b/src/MissionManager/MissionController.h @@ -118,6 +118,14 @@ class MissionController : public PlanElementController /// @return Newly created item Q_INVOKABLE VisualMissionItem* insertTakeoffItem(QGeoCoordinate coordinate, int visualItemIndex, bool makeCurrentItem = false); + /// Add a multicopter takeoff item for a VTOL which supports remaining in multicopter mode + /// @param coordinate: Coordinate for item + /// @param visualItemIndex: index to insert at, -1 for end of list + /// @param makeCurrentItem: true: Make this item the current item + /// @return Newly created item, or nullptr if the vehicle does not support this command behavior + Q_INVOKABLE VisualMissionItem* insertVTOLMulticopterTakeoffItem( + QGeoCoordinate coordinate, int visualItemIndex, bool makeCurrentItem = false); + /// Add a new land item to the list /// @param coordinate: Coordinate for item /// @param visualItemIndex: index to insert at, -1 for end of list @@ -385,6 +393,7 @@ private slots: void _initLoadedVisualItems (QmlObjectListModel* loadedVisualItems); FlightPathSegment* _addFlightPathSegment (FlightPathSegmentHashTable& prevItemPairHashTable, VisualItemPair& pair, bool mavlinkTerrainFrame); VisualMissionItem* _insertSimpleMissionItemWorker (QGeoCoordinate coordinate, MAV_CMD command, int visualItemIndex, bool makeCurrentItem); + VisualMissionItem* _insertTakeoffItemWorker (MAV_CMD command, int visualItemIndex, bool makeCurrentItem); void _insertComplexMissionItemWorker (const QGeoCoordinate& mapCenterCoordinate, ComplexMissionItem* complexItem, int visualItemIndex, bool makeCurrentItem); bool _isROIBeginItem (SimpleMissionItem* simpleItem); bool _isROICancelItem (SimpleMissionItem* simpleItem); @@ -450,7 +459,7 @@ private slots: bool _isROIBeginCurrentItem = false; double _minAMSLAltitude = 0; double _maxAMSLAltitude = 0; - bool _missionContainsVTOLTakeoff = false; + bool _missionStartsInVTOLMulticopterMode = false; QGroundControlQmlGlobal::AltitudeFrame _globalAltFrame = QGroundControlQmlGlobal::AltitudeFrameRelative; diff --git a/src/MissionManager/MissionFlightStatusCalculator.cc b/src/MissionManager/MissionFlightStatusCalculator.cc index 99ebedafff4..e45f1ec1d5a 100644 --- a/src/MissionManager/MissionFlightStatusCalculator.cc +++ b/src/MissionManager/MissionFlightStatusCalculator.cc @@ -12,7 +12,8 @@ #include -void MissionFlightStatusCalculator::reset(Vehicle* controllerVehicle, Vehicle* managerVehicle, bool missionContainsVTOLTakeoff) +void MissionFlightStatusCalculator::reset( + Vehicle* controllerVehicle, Vehicle* managerVehicle, bool missionStartsInVTOLMulticopterMode) { _status.totalDistance = 0.0; _status.plannedDistance = 0.0; @@ -36,7 +37,9 @@ void MissionFlightStatusCalculator::reset(Vehicle* controllerVehicle, Vehicle* m _status.cruiseAmpsTotal = 0; _status.batteryChangePoint = -1; _status.batteriesRequired = -1; - _status.vtolMode = missionContainsVTOLTakeoff ? QGCMAVLink::VehicleClassMultiRotor : QGCMAVLink::VehicleClassFixedWing; + _status.vtolMode = missionStartsInVTOLMulticopterMode + ? QGCMAVLink::VehicleClassMultiRotor + : QGCMAVLink::VehicleClassFixedWing; controllerVehicle->firmwarePlugin()->batteryConsumptionData(controllerVehicle, _status.mAhBattery, _status.hoverAmps, _status.cruiseAmps); if (_status.mAhBattery != 0) { @@ -51,7 +54,7 @@ void MissionFlightStatusCalculator::recalc(QmlObjectListModel* visualItems, Vehicle* managerVehicle, AppSettings* appSettings, PlanViewSettings* planViewSettings, - bool missionContainsVTOLTakeoff) + bool missionStartsInVTOLMulticopterMode) { bool firstCoordinateItem = true; VisualMissionItem* lastFlyThroughVI = qobject_cast(visualItems->get(0)); @@ -70,7 +73,7 @@ void MissionFlightStatusCalculator::recalc(QmlObjectListModel* visualItems, _minAMSLAltitude = _maxAMSLAltitude = qQNaN(); - reset(controllerVehicle, managerVehicle, missionContainsVTOLTakeoff); + reset(controllerVehicle, managerVehicle, missionStartsInVTOLMulticopterMode); bool linkStartToHome = false; bool foundRTL = false; @@ -242,7 +245,12 @@ void MissionFlightStatusCalculator::recalc(QmlObjectListModel* visualItems, // Update VTOL state if (simpleItem && controllerVehicle->vtol()) { switch (simpleItem->command()) { - case MAV_CMD_NAV_TAKEOFF: // This will do a fixed wing style takeoff + case MAV_CMD_NAV_TAKEOFF: + if (!controllerVehicle->firmwarePlugin()->isCapable( + controllerVehicle, FirmwarePlugin::VTOLMulticopterTakeoffCapability)) { + _status.vtolMode = QGCMAVLink::VehicleClassFixedWing; + } + break; case MAV_CMD_NAV_VTOL_TAKEOFF: // Vehicle goes straight up and then transitions to FW case MAV_CMD_NAV_LAND: _status.vtolMode = QGCMAVLink::VehicleClassFixedWing; diff --git a/src/MissionManager/MissionFlightStatusCalculator.h b/src/MissionManager/MissionFlightStatusCalculator.h index 38fb6f590fb..723181e1fbb 100644 --- a/src/MissionManager/MissionFlightStatusCalculator.h +++ b/src/MissionManager/MissionFlightStatusCalculator.h @@ -20,7 +20,7 @@ class MissionFlightStatusCalculator { public: /// Resets the flight status fields to defaults based on vehicle properties. - void reset(Vehicle* controllerVehicle, Vehicle* managerVehicle, bool missionContainsVTOLTakeoff); + void reset(Vehicle* controllerVehicle, Vehicle* managerVehicle, bool missionStartsInVTOLMulticopterMode); /// Runs the full recalculation over all visual items, updating per-item /// display properties and computing aggregate flight statistics. @@ -30,7 +30,7 @@ class MissionFlightStatusCalculator Vehicle* managerVehicle, AppSettings* appSettings, PlanViewSettings* planViewSettings, - bool missionContainsVTOLTakeoff); + bool missionStartsInVTOLMulticopterMode); const MissionFlightStatus_t& status() const { return _status; } double minAMSLAltitude() const { return _minAMSLAltitude; } diff --git a/src/MissionManager/TakeoffMissionItem.cc b/src/MissionManager/TakeoffMissionItem.cc index 1536a6fa6b5..327260d0771 100644 --- a/src/MissionManager/TakeoffMissionItem.cc +++ b/src/MissionManager/TakeoffMissionItem.cc @@ -1,11 +1,13 @@ #include "TakeoffMissionItem.h" + +#include "FirmwarePlugin.h" #include "MissionCommandTree.h" -#include "QGroundControlQmlGlobal.h" -#include "SettingsManager.h" -#include "PlanViewSettings.h" -#include "PlanMasterController.h" #include "MissionSettingsItem.h" #include "MultiVehicleManager.h" +#include "PlanMasterController.h" +#include "PlanViewSettings.h" +#include "QGroundControlQmlGlobal.h" +#include "SettingsManager.h" #include "Vehicle.h" TakeoffMissionItem::TakeoffMissionItem(PlanMasterController* masterController, bool flyView, MissionSettingsItem* settingsItem, bool forLoad) @@ -112,7 +114,12 @@ bool TakeoffMissionItem::isTakeoffCommand(MAV_CMD command) void TakeoffMissionItem::_initLaunchTakeoffAtSameLocation(void) { if (specifiesCoordinate()) { - if (_controllerVehicle->fixedWing() || _controllerVehicle->vtol()) { + const bool isVTOLMulticopterTakeoff = + _controllerVehicle->vtol() + && mavCommand() == MAV_CMD_NAV_TAKEOFF + && _controllerVehicle->firmwarePlugin()->isCapable( + _controllerVehicle, FirmwarePlugin::VTOLMulticopterTakeoffCapability); + if (_controllerVehicle->fixedWing() || (_controllerVehicle->vtol() && !isVTOLMulticopterTakeoff)) { _setLaunchTakeoffAtSameLocation(false); } else { // PX4 specifies a coordinate for takeoff even for multi-rotor. But it makes more sense to not have a coordinate diff --git a/src/PlanView/PlanView.qml b/src/PlanView/PlanView.qml index 257a99cf302..c6e26a81913 100644 --- a/src/PlanView/PlanView.qml +++ b/src/PlanView/PlanView.qml @@ -194,6 +194,11 @@ Item { _missionController.insertTakeoffItem(mapCenter(), nextIndex, true /* makeCurrentItem */) } + function insertVTOLMulticopterTakeoffItemAfterCurrent() { + var nextIndex = _missionController.currentPlanViewVIIndex + 1 + _missionController.insertVTOLMulticopterTakeoffItem(mapCenter(), nextIndex, true /* makeCurrentItem */) + } + function insertLandItemAfterCurrent() { var nextIndex = _missionController.currentPlanViewVIIndex + 1 _missionController.insertLandItem(mapCenter(), nextIndex, true /* makeCurrentItem */) @@ -454,6 +459,9 @@ Item { iconSource: "/res/takeoff.svg" enabled: _missionController.isInsertTakeoffValid visible: toolStrip._isMissionLayer && !_planMasterController.controllerVehicle.rover + dropPanelComponent: _planMasterController.controllerVehicle.supports.vtolMulticopterTakeoff + ? vtolTakeoffDropPanel + : null onTriggered: { insertTakeoffItemAfterCurrent() } @@ -808,6 +816,39 @@ Item { } // Column } + Component { + id: vtolTakeoffDropPanel + + ColumnLayout { + spacing: ScreenTools.defaultFontPixelWidth * 0.5 + + QGCLabel { text: qsTr("Choose takeoff mode:") } + + QGCButton { + objectName: "planTakeoff_vtolButton" + text: qsTr("VTOL takeoff") + Layout.fillWidth: true + primary: true + + onClicked: { + insertTakeoffItemAfterCurrent() + dropPanel.hide() + } + } + + QGCButton { + objectName: "planTakeoff_mcButton" + text: qsTr("Multicopter takeoff") + Layout.fillWidth: true + + onClicked: { + insertVTOLMulticopterTakeoffItemAfterCurrent() + dropPanel.hide() + } + } + } + } + QGCPopupDialogFactory { id: promptForPlanUsageOnVehicleChangePopupFactory diff --git a/src/Vehicle/VehicleSupports.cc b/src/Vehicle/VehicleSupports.cc index 7529ee26218..a6e96744866 100644 --- a/src/Vehicle/VehicleSupports.cc +++ b/src/Vehicle/VehicleSupports.cc @@ -86,3 +86,8 @@ bool VehicleSupports::changeHeading() const { return _vehicle->firmwarePlugin()->isCapable(_vehicle, FirmwarePlugin::ChangeHeadingCapability); } + +bool VehicleSupports::vtolMulticopterTakeoff() const +{ + return _vehicle->firmwarePlugin()->isCapable(_vehicle, FirmwarePlugin::VTOLMulticopterTakeoffCapability); +} diff --git a/src/Vehicle/VehicleSupports.h b/src/Vehicle/VehicleSupports.h index dc7c45b4e68..bc17b176554 100644 --- a/src/Vehicle/VehicleSupports.h +++ b/src/Vehicle/VehicleSupports.h @@ -30,6 +30,7 @@ class VehicleSupports : public QObject Q_PROPERTY(bool guidedTakeoffWithAltitude READ guidedTakeoffWithAltitude CONSTANT) Q_PROPERTY(bool guidedTakeoffWithoutAltitude READ guidedTakeoffWithoutAltitude CONSTANT) Q_PROPERTY(bool changeHeading READ changeHeading CONSTANT) + Q_PROPERTY(bool vtolMulticopterTakeoff READ vtolMulticopterTakeoff CONSTANT) bool throttleModeCenterZero() const; bool negativeThrust() const; @@ -46,6 +47,7 @@ class VehicleSupports : public QObject bool guidedTakeoffWithAltitude() const; bool guidedTakeoffWithoutAltitude() const; bool changeHeading() const; + bool vtolMulticopterTakeoff() const; signals: void terrainFrameChanged(); diff --git a/test/MissionManager/MissionCommandTreeTest.cc b/test/MissionManager/MissionCommandTreeTest.cc index e749e98b71b..bdb9c8cf665 100644 --- a/test/MissionManager/MissionCommandTreeTest.cc +++ b/test/MissionManager/MissionCommandTreeTest.cc @@ -176,6 +176,16 @@ void MissionCommandTreeTest::testOverride() delete vehicle; } +void MissionCommandTreeTest::testPX4VTOLTakeoffOverride() +{ + MissionCommandList commandList(QStringLiteral(":/json/PX4-MavCmdInfoVTOL.json"), false /* baseCommandList */); + const MissionCommandUIInfo* const takeoffInfo = commandList.getUIInfo(MAV_CMD_NAV_TAKEOFF); + + QVERIFY(takeoffInfo); + QCOMPARE(takeoffInfo->description(), + QStringLiteral("Take off vertically and continue the mission in multicopter mode.")); +} + void MissionCommandTreeTest::testAllTrees() { ignoreLogMessage("FirmwarePlugin.ParameterMetaData", QtWarningMsg, QRegularExpression("Skipping invalid enum value")); diff --git a/test/MissionManager/MissionCommandTreeTest.h b/test/MissionManager/MissionCommandTreeTest.h index bd607b1cc0b..06be0696da2 100644 --- a/test/MissionManager/MissionCommandTreeTest.h +++ b/test/MissionManager/MissionCommandTreeTest.h @@ -18,6 +18,7 @@ private slots: void testJsonLoad(); void testOverride(); + void testPX4VTOLTakeoffOverride(); void testAllTrees(); void testUnknownCommandFallbacks(); diff --git a/test/MissionManager/MissionControllerTest.cc b/test/MissionManager/MissionControllerTest.cc index f33bc3bb523..5e4bbda3a69 100644 --- a/test/MissionManager/MissionControllerTest.cc +++ b/test/MissionManager/MissionControllerTest.cc @@ -14,8 +14,11 @@ #include "PlanViewSettings.h" #include "SettingsManager.h" #include "SimpleMissionItem.h" +#include "TakeoffMissionItem.h" #include "TestFixtures.h" #include "MultiSignalSpy.h" +#include "Vehicle.h" +#include "VehicleSupports.h" #include #include @@ -45,6 +48,17 @@ void MissionControllerTest::_initForFirmwareType(MAV_AUTOPILOT firmwareType) SettingsManager::instance()->appSettings()->offlineEditingFirmwareClass()->setRawValue( QGCMAVLink::firmwareClass(firmwareType)); _masterController = std::make_unique(); + _startMasterController(); +} + +void MissionControllerTest::_initForVehicleType(MAV_AUTOPILOT firmwareType, MAV_TYPE vehicleType) +{ + _masterController = std::make_unique(firmwareType, vehicleType); + _startMasterController(); +} + +void MissionControllerTest::_startMasterController() +{ _masterController->setFlyView(false); _missionController = _masterController->missionController(); MultiSignalSpy missionControllerSpy; @@ -69,6 +83,151 @@ void MissionControllerTest::_initForFirmwareType(MAV_AUTOPILOT firmwareType) QCOMPARE(simpleFlightPathSegments->count(), 0); } +void MissionControllerTest::_testVTOLTakeoffModes_data() +{ + QTest::addColumn("firmwareType"); + QTest::addColumn("vehicleType"); + QTest::addColumn("useMulticopterTakeoff"); + QTest::addColumn("supportsMulticopterTakeoff"); + QTest::addColumn("expectedCommand"); + QTest::addColumn("expectedSameLocation"); + QTest::addColumn("expectedWaypointVTOLMode"); + + QTest::newRow("PX4 VTOL default") + << int(MAV_AUTOPILOT_PX4) << int(MAV_TYPE_VTOL_TAILSITTER_QUADROTOR) + << false << true << int(MAV_CMD_NAV_VTOL_TAKEOFF) << false << int(QGCMAVLink::VehicleClassFixedWing); + QTest::newRow("PX4 VTOL multicopter") + << int(MAV_AUTOPILOT_PX4) << int(MAV_TYPE_VTOL_TAILSITTER_QUADROTOR) + << true << true << int(MAV_CMD_NAV_TAKEOFF) << true << int(QGCMAVLink::VehicleClassMultiRotor); + QTest::newRow("PX4 multicopter unchanged") + << int(MAV_AUTOPILOT_PX4) << int(MAV_TYPE_QUADROTOR) + << false << false << int(MAV_CMD_NAV_TAKEOFF) << true << int(QGCMAVLink::VehicleClassGeneric); + QTest::newRow("PX4 fixed wing unchanged") + << int(MAV_AUTOPILOT_PX4) << int(MAV_TYPE_FIXED_WING) + << false << false << int(MAV_CMD_NAV_TAKEOFF) << false << int(QGCMAVLink::VehicleClassGeneric); +} + +void MissionControllerTest::_testVTOLTakeoffModes() +{ + QFETCH(int, firmwareType); + QFETCH(int, vehicleType); + QFETCH(bool, useMulticopterTakeoff); + QFETCH(bool, supportsMulticopterTakeoff); + QFETCH(int, expectedCommand); + QFETCH(bool, expectedSameLocation); + QFETCH(int, expectedWaypointVTOLMode); + + _initForVehicleType(static_cast(firmwareType), static_cast(vehicleType)); + + Vehicle* controllerVehicle = _masterController->controllerVehicle(); + QVERIFY(controllerVehicle); + QCOMPARE(controllerVehicle->supports()->vtolMulticopterTakeoff(), supportsMulticopterTakeoff); + + const QGeoCoordinate home = Coord::zurich(); + _missionController->setHomePosition(home); + VisualMissionItem* visualTakeoff = useMulticopterTakeoff + ? _missionController->insertVTOLMulticopterTakeoffItem(home, 1) + : _missionController->insertTakeoffItem(home, 1); + QVERIFY(visualTakeoff); + + TakeoffMissionItem* takeoffItem = qobject_cast(visualTakeoff); + QVERIFY(takeoffItem); + QCOMPARE(takeoffItem->mavCommand(), static_cast(expectedCommand)); + QCOMPARE(takeoffItem->launchTakeoffAtSameLocation(), expectedSameLocation); + + SimpleMissionItem* waypoint = qobject_cast( + _missionController->insertSimpleMissionItem(home.atDistanceAndAzimuth(100.0, 0.0), 2)); + QVERIFY(waypoint); + + if (controllerVehicle->vtol()) { + QCOMPARE_TRUE_WAIT(waypoint->property("previousVTOLMode").toInt(), expectedWaypointVTOLMode, TestTimeout::mediumMs()); + } +} + +void MissionControllerTest::_testArduPilotVTOLOrdinaryTakeoffCompatibility() +{ + _initForVehicleType(MAV_AUTOPILOT_ARDUPILOTMEGA, MAV_TYPE_VTOL_TAILSITTER_QUADROTOR); + + Vehicle* controllerVehicle = _masterController->controllerVehicle(); + QVERIFY(controllerVehicle); + QCOMPARE(controllerVehicle->supports()->vtolMulticopterTakeoff(), false); + + const QGeoCoordinate home = Coord::zurich(); + _missionController->setHomePosition(home); + MissionSettingsItem* settingsItem = _missionController->visualItems()->value(0); + QVERIFY(settingsItem); + + TakeoffMissionItem* defaultTakeoffItem = qobject_cast( + _missionController->insertTakeoffItem(home, 1)); + QVERIFY(defaultTakeoffItem); + QCOMPARE(defaultTakeoffItem->mavCommand(), MAV_CMD_NAV_VTOL_TAKEOFF); + + SimpleMissionItem* waypoint = qobject_cast( + _missionController->insertSimpleMissionItem(home.atDistanceAndAzimuth(100.0, 0.0), 2)); + QVERIFY(waypoint); + QCOMPARE_TRUE_WAIT( + waypoint->property("previousVTOLMode").toInt(), + int(QGCMAVLink::VehicleClassFixedWing), + TestTimeout::mediumMs()); + + TakeoffMissionItem ordinaryTakeoffItem( + MAV_CMD_NAV_TAKEOFF, _masterController.get(), false /* flyView */, settingsItem, false /* forLoad */); + QCOMPARE(ordinaryTakeoffItem.launchTakeoffAtSameLocation(), !ordinaryTakeoffItem.specifiesCoordinate()); +} + +void MissionControllerTest::_testUnsupportedVTOLMulticopterTakeoff() +{ + _initForVehicleType(MAV_AUTOPILOT_PX4, MAV_TYPE_QUADROTOR); + _missionController->setHomePosition(Coord::zurich()); + const int initialCount = _missionController->visualItems()->count(); + + expectAppMessage(QRegularExpression(QStringLiteral("Multicopter takeoff is not supported"))); + QVERIFY(!_missionController->insertVTOLMulticopterTakeoffItem(Coord::zurich(), 1)); + verifyExpectedLogMessage(); + QCOMPARE(_missionController->visualItems()->count(), initialCount); +} + +void MissionControllerTest::_testVTOLTakeoffJsonRoundTrip_data() +{ + QTest::addColumn("useMulticopterTakeoff"); + QTest::addColumn("expectedCommand"); + + QTest::newRow("VTOL takeoff") << false << int(MAV_CMD_NAV_VTOL_TAKEOFF); + QTest::newRow("multicopter takeoff") << true << int(MAV_CMD_NAV_TAKEOFF); +} + +void MissionControllerTest::_testVTOLTakeoffJsonRoundTrip() +{ + QFETCH(bool, useMulticopterTakeoff); + QFETCH(int, expectedCommand); + + _initForVehicleType(MAV_AUTOPILOT_PX4, MAV_TYPE_VTOL_TAILSITTER_QUADROTOR); + + const QGeoCoordinate home = Coord::zurich(); + _missionController->setHomePosition(home); + VisualMissionItem* visualTakeoff = useMulticopterTakeoff + ? _missionController->insertVTOLMulticopterTakeoffItem(home, 1) + : _missionController->insertTakeoffItem(home, 1); + TakeoffMissionItem* takeoffItem = qobject_cast(visualTakeoff); + QVERIFY(takeoffItem); + takeoffItem->setCoordinate(home.atDistanceAndAzimuth(50.0, 0.0)); + QVERIFY(_missionController->insertSimpleMissionItem(home.atDistanceAndAzimuth(100.0, 0.0), 2)); + + QTemporaryDir tempDir; + QVERIFY(tempDir.isValid()); + const QString filename = tempDir.filePath(QStringLiteral("VTOLTakeoff.plan")); + QVERIFY(_masterController->saveToFile(filename)); + + _masterController->removeAll(); + _masterController->loadFromFile(filename); + + QmlObjectListModel* visualItems = _missionController->visualItems(); + QCOMPARE(visualItems->count(), 3); + TakeoffMissionItem* loadedTakeoff = visualItems->value(1); + QVERIFY(loadedTakeoff); + QCOMPARE(loadedTakeoff->mavCommand(), static_cast(expectedCommand)); +} + void MissionControllerTest::_testEmptyVehicle_data() { TestData::addAutopilotRows(); diff --git a/test/MissionManager/MissionControllerTest.h b/test/MissionManager/MissionControllerTest.h index 24acab4bb3e..b4fb4a75782 100644 --- a/test/MissionManager/MissionControllerTest.h +++ b/test/MissionManager/MissionControllerTest.h @@ -35,12 +35,20 @@ private slots: void _testInsertValidityHomePositionGating(); void _testLandToolInsertsSingleRtl_data(); void _testLandToolInsertsSingleRtl(); + void _testVTOLTakeoffModes_data(); + void _testVTOLTakeoffModes(); + void _testArduPilotVTOLOrdinaryTakeoffCompatibility(); + void _testUnsupportedVTOLMulticopterTakeoff(); + void _testVTOLTakeoffJsonRoundTrip_data(); + void _testVTOLTakeoffJsonRoundTrip(); // Parameterized tests - runs once per autopilot type UT_PARAMETERIZED_TEST(_testEmptyVehicle); private: void _initForFirmwareType(MAV_AUTOPILOT firmwareType); + void _initForVehicleType(MAV_AUTOPILOT firmwareType, MAV_TYPE vehicleType); + void _startMasterController(); void _setupVisualItemSignals(VisualMissionItem* visualItem); std::unique_ptr _masterController; diff --git a/test/MissionManager/PlanMasterControllerTest.cc b/test/MissionManager/PlanMasterControllerTest.cc index c96c808f6c6..5796db4230d 100644 --- a/test/MissionManager/PlanMasterControllerTest.cc +++ b/test/MissionManager/PlanMasterControllerTest.cc @@ -41,14 +41,33 @@ void PlanMasterControllerTest::_testMissionPlannerFileLoad() QCOMPARE(_masterController->missionController()->visualItems()->count(), 6); } +void PlanMasterControllerTest::_testTakeoffTextFileLoad_data() +{ + QTest::addColumn("firmwareClass"); + QTest::addColumn("vehicleClass"); + QTest::addColumn("takeoffCommand"); + + QTest::newRow("ArduPilot takeoff") + << int(QGCMAVLink::FirmwareClassArduPilot) << int(QGCMAVLink::VehicleClassMultiRotor) << int(MAV_CMD_NAV_TAKEOFF); + QTest::newRow("PX4 VTOL multicopter takeoff") + << int(QGCMAVLink::FirmwareClassPX4) << int(QGCMAVLink::VehicleClassVTOL) << int(MAV_CMD_NAV_TAKEOFF); + QTest::newRow("PX4 VTOL takeoff") + << int(QGCMAVLink::FirmwareClassPX4) << int(QGCMAVLink::VehicleClassVTOL) << int(MAV_CMD_NAV_VTOL_TAKEOFF); +} + void PlanMasterControllerTest::_testTakeoffTextFileLoad() { + QFETCH(int, firmwareClass); + QFETCH(int, vehicleClass); + QFETCH(int, takeoffCommand); + // Plain-text mission file with home position, takeoff and one waypoint (#13167) - static const char* kTakeoffMission = + const QByteArray takeoffMission = QByteArray( "QGC WPL 110\r\n" "0\t1\t0\t16\t0\t0\t0\t0\t34.577822\t-112.469101\t584.380005\t1\r\n" - "1\t0\t3\t22\t20.000000\t0.000000\t0.000000\t0.000000\t0.000000\t0.000000\t30.000000\t1\r\n" - "2\t0\t3\t16\t0.000000\t0.000000\t0.000000\t0.000000\t34.469587\t-112.534801\t90.000000\t1\r\n"; + "1\t0\t3\t") + QByteArray::number(takeoffCommand) + QByteArray( + "\t20.000000\t0.000000\t0.000000\t0.000000\t0.000000\t0.000000\t30.000000\t1\r\n" + "2\t0\t3\t16\t0.000000\t0.000000\t0.000000\t0.000000\t34.469587\t-112.534801\t90.000000\t1\r\n"); QTemporaryDir tempDir; QVERIFY(tempDir.isValid()); @@ -57,10 +76,11 @@ void PlanMasterControllerTest::_testTakeoffTextFileLoad() // No QIODevice::Text: write the CRLF line endings verbatim on all platforms to match // the original repro file from the issue. QVERIFY(file.open(QIODevice::WriteOnly)); - QVERIFY(file.write(kTakeoffMission) != -1); + QVERIFY(file.write(takeoffMission) != -1); file.close(); - SettingsManager::instance()->appSettings()->offlineEditingFirmwareClass()->setRawValue(QGCMAVLink::FirmwareClassArduPilot); + SettingsManager::instance()->appSettings()->offlineEditingFirmwareClass()->setRawValue(firmwareClass); + SettingsManager::instance()->appSettings()->offlineEditingVehicleClass()->setRawValue(vehicleClass); _masterController->loadFromFile(filename); @@ -71,7 +91,7 @@ void PlanMasterControllerTest::_testTakeoffTextFileLoad() // resulting in a single takeoff item carrying the waypoint's values. TakeoffMissionItem* takeoffItem = visualItems->value(1); QVERIFY(takeoffItem); - QCOMPARE(static_cast(takeoffItem->command()), MAV_CMD_NAV_TAKEOFF); + QCOMPARE(static_cast(takeoffItem->command()), static_cast(takeoffCommand)); QCOMPARE(takeoffItem->missionItem().param7(), 30.0); SimpleMissionItem* waypointItem = visualItems->value(2); diff --git a/test/MissionManager/PlanMasterControllerTest.h b/test/MissionManager/PlanMasterControllerTest.h index e670993701b..2c370b113b6 100644 --- a/test/MissionManager/PlanMasterControllerTest.h +++ b/test/MissionManager/PlanMasterControllerTest.h @@ -13,6 +13,7 @@ private slots: void cleanup() final; void _testMissionPlannerFileLoad(); + void _testTakeoffTextFileLoad_data(); void _testTakeoffTextFileLoad(); void _testActiveVehicleChanged(); void _testDirtyFlagsMatrix_data(); diff --git a/test/QmlUITests/PlanViewUITest.cc b/test/QmlUITests/PlanViewUITest.cc index 976effbd629..ec592af8ffc 100644 --- a/test/QmlUITests/PlanViewUITest.cc +++ b/test/QmlUITests/PlanViewUITest.cc @@ -10,10 +10,13 @@ #include "AppSettings.h" #include "Fact.h" +#include "MissionController.h" #include "PlanViewSettings.h" #include "QGCFileDialogController.h" #include "QGCMAVLink.h" +#include "QmlObjectListModel.h" #include "SettingsManager.h" +#include "SimpleMissionItem.h" UT_REGISTER_TEST(PlanViewUITest, TestLabel::Integration, TestLabel::MissionManager) @@ -38,6 +41,21 @@ int PlanViewUITest::_missionItemCount() return visualItems->property("count").toInt(); } +int PlanViewUITest::_missionItemCommand(int index) +{ + QQuickItem* planView = findVisibleItem(_rootItem, QStringLiteral("mainView_plan")); + if (!planView) { + return -1; + } + MissionController* missionController = qobject_cast( + planView->property("_missionController").value()); + if (!missionController || index < 0 || index >= missionController->visualItems()->count()) { + return -1; + } + SimpleMissionItem* item = missionController->visualItems()->value(index); + return item ? item->command() : -1; +} + QGeoCoordinate PlanViewUITest::_plannedHomePosition() { QQuickItem *planView = findVisibleItem(_rootItem, QStringLiteral("mainView_plan")); @@ -150,6 +168,50 @@ void PlanViewUITest::_testTakeoffNotRequiredWaypointOnEmptyPlan() _verifyWaypointToolAddsWaypointOnEmptyPlan(true /* expectTakeoffButtonVisible */); } +void PlanViewUITest::_testVTOLTakeoffChoices_data() +{ + QTest::addColumn("choiceButton"); + QTest::addColumn("expectedCommand"); + + QTest::newRow("VTOL takeoff") << QStringLiteral("planTakeoff_vtolButton") << int(MAV_CMD_NAV_VTOL_TAKEOFF); + QTest::newRow("multicopter takeoff") << QStringLiteral("planTakeoff_mcButton") << int(MAV_CMD_NAV_TAKEOFF); +} + +void PlanViewUITest::_testVTOLTakeoffChoices() +{ + QFETCH(QString, choiceButton); + QFETCH(int, expectedCommand); + + AppSettings* appSettings = SettingsManager::instance()->appSettings(); + appSettings->offlineEditingFirmwareClass()->setRawValue(QGCMAVLink::FirmwareClassPX4); + appSettings->offlineEditingVehicleClass()->setRawValue(QGCMAVLink::VehicleClassVTOL); + + startUI(); + if (QTest::currentTestFailed()) return; + + _navigateToPlanAndCenterMap(); + if (QTest::currentTestFailed()) return; + + _clickMap(0.5, 0.5); + if (QTest::currentTestFailed()) return; + QVERIFY2(waitForCondition([&] { return _plannedHomePosition().isValid(); }, 2000, + QStringLiteral("VTOL home position set")), + "Map click did not set the VTOL home position"); + + QVERIFY2(clickButton(QStringLiteral("planToolStrip_takeoffButton")), "Failed to open the VTOL takeoff choices"); + QQuickItem* vtolButton = findVisibleItem(_rootItem, QStringLiteral("planTakeoff_vtolButton"), 2000); + QQuickItem* multicopterButton = findVisibleItem(_rootItem, QStringLiteral("planTakeoff_mcButton"), 2000); + QVERIFY2(vtolButton, "VTOL takeoff choice was not shown"); + QVERIFY2(multicopterButton, "Multicopter takeoff choice was not shown"); + QCOMPARE(vtolButton->property("primary").toBool(), true); + + QVERIFY2(clickButton(choiceButton), "Failed to select the VTOL takeoff mode"); + QVERIFY2(waitForCondition([&] { return _missionItemCount() == 2; }, 2000, + QStringLiteral("VTOL takeoff inserted")), + "The selected VTOL takeoff command was not inserted"); + QCOMPARE(_missionItemCommand(1), expectedCommand); +} + void PlanViewUITest::_verifyFullState(const PlanUIState &state, const QString &context) { const QString takeoffBtn = QStringLiteral("planToolStrip_takeoffButton"); diff --git a/test/QmlUITests/PlanViewUITest.h b/test/QmlUITests/PlanViewUITest.h index 77e2270511e..b294ecacf08 100644 --- a/test/QmlUITests/PlanViewUITest.h +++ b/test/QmlUITests/PlanViewUITest.h @@ -23,6 +23,8 @@ private slots: void _testSaveAsMenu(); void _testRoverWaypointOnEmptyPlan(); void _testTakeoffNotRequiredWaypointOnEmptyPlan(); + void _testVTOLTakeoffChoices_data(); + void _testVTOLTakeoffChoices(); private: /// Complete expected state of all Plan view UI under test @@ -64,6 +66,9 @@ private slots: /// Current MissionController visualItems count, or -1 if unavailable. int _missionItemCount(); + /// MAVLink command for a visual mission item, or -1 if unavailable. + int _missionItemCommand(int index); + /// Current MissionController plannedHomePosition, or invalid if unavailable. QGeoCoordinate _plannedHomePosition(); };