/**************************************************************************** * * (c) 2009-2020 QGROUNDCONTROL PROJECT * * QGroundControl is licensed according to the terms in the file * COPYING.md in the root of the source code directory. * ****************************************************************************/ #include "PlanMasterController.h" #include "QGCApplication.h" #include "QGCCorePlugin.h" #include "MultiVehicleManager.h" #include "SettingsManager.h" #include "AppSettings.h" #include "JsonHelper.h" #include "MissionManager.h" #include "KMLPlanDomDocument.h" #include "SurveyPlanCreator.h" #include "StructureScanPlanCreator.h" #include "CorridorScanPlanCreator.h" #include "BlankPlanCreator.h" #if defined(QGC_AIRMAP_ENABLED) #include "AirspaceFlightPlanProvider.h" #endif #include #include #include QGC_LOGGING_CATEGORY(PlanMasterControllerLog, "PlanMasterControllerLog") const int PlanMasterController::kPlanFileVersion = 1; const char* PlanMasterController::kPlanFileType = "Plan"; const char* PlanMasterController::kJsonMissionObjectKey = "mission"; const char* PlanMasterController::kJsonGeoFenceObjectKey = "geoFence"; const char* PlanMasterController::kJsonRallyPointsObjectKey = "rallyPoints"; PlanMasterController::PlanMasterController(QObject* parent) : QObject (parent) , _multiVehicleMgr (qgcApp()->toolbox()->multiVehicleManager()) , _controllerVehicle (new Vehicle(Vehicle::MAV_AUTOPILOT_TRACK, Vehicle::MAV_TYPE_TRACK, qgcApp()->toolbox()->firmwarePluginManager(), this)) , _managerVehicle (_controllerVehicle) , _missionController (this) , _geoFenceController (this) , _rallyPointController (this) { _commonInit(); } #ifdef QT_DEBUG PlanMasterController::PlanMasterController(MAV_AUTOPILOT firmwareType, MAV_TYPE vehicleType, QObject* parent) : QObject (parent) , _multiVehicleMgr (qgcApp()->toolbox()->multiVehicleManager()) , _controllerVehicle (new Vehicle(firmwareType, vehicleType, qgcApp()->toolbox()->firmwarePluginManager())) , _managerVehicle (_controllerVehicle) , _missionController (this) , _geoFenceController (this) , _rallyPointController (this) { _commonInit(); } #endif void PlanMasterController::_commonInit(void) { connect(&_missionController, &MissionController::dirtyChanged, this, &PlanMasterController::dirtyChanged); connect(&_geoFenceController, &GeoFenceController::dirtyChanged, this, &PlanMasterController::dirtyChanged); connect(&_rallyPointController, &RallyPointController::dirtyChanged, this, &PlanMasterController::dirtyChanged); connect(&_missionController, &MissionController::containsItemsChanged, this, &PlanMasterController::containsItemsChanged); connect(&_geoFenceController, &GeoFenceController::containsItemsChanged, this, &PlanMasterController::containsItemsChanged); connect(&_rallyPointController, &RallyPointController::containsItemsChanged, this, &PlanMasterController::containsItemsChanged); connect(&_missionController, &MissionController::syncInProgressChanged, this, &PlanMasterController::syncInProgressChanged); connect(&_geoFenceController, &GeoFenceController::syncInProgressChanged, this, &PlanMasterController::syncInProgressChanged); connect(&_rallyPointController, &RallyPointController::syncInProgressChanged, this, &PlanMasterController::syncInProgressChanged); // Offline vehicle can change firmware/vehicle type connect(_controllerVehicle, &Vehicle::vehicleTypeChanged, this, &PlanMasterController::_updatePlanCreatorsList); } PlanMasterController::~PlanMasterController() { } void PlanMasterController::start(void) { _missionController.start (_flyView); _geoFenceController.start (_flyView); _rallyPointController.start (_flyView); _activeVehicleChanged(_multiVehicleMgr->activeVehicle()); connect(_multiVehicleMgr, &MultiVehicleManager::activeVehicleChanged, this, &PlanMasterController::_activeVehicleChanged); _updatePlanCreatorsList(); #if defined(QGC_AIRMAP_ENABLED) //-- This assumes there is one single instance of PlanMasterController in edit mode. if(!flyView) { // Wait for signal confirming AirMap client connection before starting flight planning connect(qgcApp()->toolbox()->airspaceManager(), &AirspaceManager::connectStatusChanged, this, &PlanMasterController::_startFlightPlanning); } #endif } void PlanMasterController::startStaticActiveVehicle(Vehicle* vehicle, bool deleteWhenSendCompleted) { _flyView = true; _deleteWhenSendCompleted = deleteWhenSendCompleted; _missionController.start(_flyView); _geoFenceController.start(_flyView); _rallyPointController.start(_flyView); _activeVehicleChanged(vehicle); } void PlanMasterController::_activeVehicleChanged(Vehicle* activeVehicle) { if (_managerVehicle == activeVehicle) { // We are already setup for this vehicle return; } qCDebug(PlanMasterControllerLog) << "_activeVehicleChanged" << activeVehicle; if (_managerVehicle) { // Disconnect old vehicle. Be careful of wildcarding disconnect too much since _managerVehicle may equal _controllerVehicle disconnect(_managerVehicle->missionManager(), nullptr, nullptr, nullptr); disconnect(_managerVehicle->geoFenceManager(), nullptr, nullptr, nullptr); disconnect(_managerVehicle->rallyPointManager(), nullptr, nullptr, nullptr); } bool newOffline = false; if (activeVehicle == nullptr) { // Since there is no longer an active vehicle we use the offline controller vehicle as the manager vehicle _managerVehicle = _controllerVehicle; newOffline = true; } else { newOffline = false; _managerVehicle = activeVehicle; // Update controllerVehicle to the currently connected vehicle AppSettings* appSettings = qgcApp()->toolbox()->settingsManager()->appSettings(); appSettings->offlineEditingFirmwareType()->setRawValue(AppSettings::offlineEditingFirmwareTypeFromFirmwareType(_managerVehicle->firmwareType())); appSettings->offlineEditingVehicleType()->setRawValue(AppSettings::offlineEditingVehicleTypeFromVehicleType(_managerVehicle->vehicleType())); // We use these signals to sequence upload and download to the multiple controller/managers connect(_managerVehicle->missionManager(), &MissionManager::newMissionItemsAvailable, this, &PlanMasterController::_loadMissionComplete); connect(_managerVehicle->geoFenceManager(), &GeoFenceManager::loadComplete, this, &PlanMasterController::_loadGeoFenceComplete); connect(_managerVehicle->rallyPointManager(), &RallyPointManager::loadComplete, this, &PlanMasterController::_loadRallyPointsComplete); connect(_managerVehicle->missionManager(), &MissionManager::sendComplete, this, &PlanMasterController::_sendMissionComplete); connect(_managerVehicle->geoFenceManager(), &GeoFenceManager::sendComplete, this, &PlanMasterController::_sendGeoFenceComplete); connect(_managerVehicle->rallyPointManager(), &RallyPointManager::sendComplete, this, &PlanMasterController::_sendRallyPointsComplete); } emit managerVehicleChanged(_managerVehicle); // Vehicle changed so we need to signal everything _offline = newOffline; emit containsItemsChanged(containsItems()); emit syncInProgressChanged(); emit dirtyChanged(dirty()); emit offlineChanged(offline()); if (!_flyView) { if (!offline()) { // We are in Plan view and we have a newly connected vehicle: // - If there is no plan available in Plan view show the one from the vehicle // - Otherwise leave the current plan alone if (!containsItems()) { qCDebug(PlanMasterControllerLog) << "_activeVehicleChanged: Plan view is empty so loading from manager"; _showPlanFromManagerVehicle(); } } } else { if (offline()) { // No more active vehicle, clear mission qCDebug(PlanMasterControllerLog) << "_activeVehicleChanged: Fly view is offline clearing plan"; removeAll(); } else { // Fly view has changed to a new active vehicle, update to show correct mission qCDebug(PlanMasterControllerLog) << "_activeVehicleChanged: Fly view is online so loading from manager"; _showPlanFromManagerVehicle(); } } _updatePlanCreatorsList(); } void PlanMasterController::loadFromVehicle(void) { if (_managerVehicle->highLatencyLink()) { qgcApp()->showAppMessage(tr("Download not supported on high latency links.")); return; } if (offline()) { qCWarning(PlanMasterControllerLog) << "PlanMasterController::loadFromVehicle called while offline"; } else if (_flyView) { qCWarning(PlanMasterControllerLog) << "PlanMasterController::loadFromVehicle called from Fly view"; } else if (syncInProgress()) { qCWarning(PlanMasterControllerLog) << "PlanMasterController::loadFromVehicle called while syncInProgress"; } else { _loadGeoFence = true; qCDebug(PlanMasterControllerLog) << "PlanMasterController::loadFromVehicle calling _missionController.loadFromVehicle"; _missionController.loadFromVehicle(); setDirty(false); } } void PlanMasterController::_loadMissionComplete(void) { if (!_flyView && _loadGeoFence) { _loadGeoFence = false; _loadRallyPoints = true; if (_geoFenceController.supported()) { qCDebug(PlanMasterControllerLog) << "PlanMasterController::_loadMissionComplete calling _geoFenceController.loadFromVehicle"; _geoFenceController.loadFromVehicle(); } else { qCDebug(PlanMasterControllerLog) << "PlanMasterController::_loadMissionComplete GeoFence not supported skipping"; _geoFenceController.removeAll(); _loadGeoFenceComplete(); } setDirty(false); } } void PlanMasterController::_loadGeoFenceComplete(void) { if (!_flyView && _loadRallyPoints) { _loadRallyPoints = false; if (_rallyPointController.supported()) { qCDebug(PlanMasterControllerLog) << "PlanMasterController::_loadGeoFenceComplete calling _rallyPointController.loadFromVehicle"; _rallyPointController.loadFromVehicle(); } else { qCDebug(PlanMasterControllerLog) << "PlanMasterController::_loadMissionComplete Rally Points not supported skipping"; _rallyPointController.removeAll(); _loadRallyPointsComplete(); } setDirty(false); } } void PlanMasterController::_loadRallyPointsComplete(void) { qCDebug(PlanMasterControllerLog) << "PlanMasterController::_loadRallyPointsComplete"; } void PlanMasterController::_sendMissionComplete(void) { if (_sendGeoFence) { _sendGeoFence = false; _sendRallyPoints = true; if (_geoFenceController.supported()) { qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle start GeoFence sendToVehicle"; _geoFenceController.sendToVehicle(); } else { qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle GeoFence not supported skipping"; _sendGeoFenceComplete(); } setDirty(false); } } void PlanMasterController::_sendGeoFenceComplete(void) { if (_sendRallyPoints) { _sendRallyPoints = false; if (_rallyPointController.supported()) { qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle start rally sendToVehicle"; _rallyPointController.sendToVehicle(); } else { qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle Rally Points not support skipping"; _sendRallyPointsComplete(); } } } void PlanMasterController::_sendRallyPointsComplete(void) { qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle Rally Point send complete"; if (_deleteWhenSendCompleted) { this->deleteLater(); } } #if defined(QGC_AIRMAP_ENABLED) void PlanMasterController::_startFlightPlanning(void) { if (qgcApp()->toolbox()->airspaceManager()->connected()) { qCDebug(PlanMasterControllerLog) << "PlanMasterController::_startFlightPlanning client connected, start flight planning"; qgcApp()->toolbox()->airspaceManager()->flightPlan()->startFlightPlanning(this); } } #endif void PlanMasterController::sendToVehicle(void) { if (_managerVehicle->highLatencyLink()) { qgcApp()->showAppMessage(tr("Upload not supported on high latency links.")); return; } if (offline()) { qCWarning(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle called while offline"; } else if (syncInProgress()) { qCWarning(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle called while syncInProgress"; } else { qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle start mission sendToVehicle"; _sendGeoFence = true; _missionController.sendToVehicle(); setDirty(false); } } void PlanMasterController::loadFromFile(const QString& filename) { QString errorString; QString errorMessage = tr("Error loading Plan file (%1). %2").arg(filename).arg("%1"); if (filename.isEmpty()) { return; } QFileInfo fileInfo(filename); QFile file(filename); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { errorString = file.errorString() + QStringLiteral(" ") + filename; qgcApp()->showAppMessage(errorMessage.arg(errorString)); return; } bool success = false; if(fileInfo.suffix() == AppSettings::planFileExtension) { QJsonDocument jsonDoc; QByteArray bytes = file.readAll(); if (!JsonHelper::isJsonFile(bytes, jsonDoc, errorString)) { qgcApp()->showAppMessage(errorMessage.arg(errorString)); return; } QJsonObject json = jsonDoc.object(); //-- Allow plugins to pre process the load qgcApp()->toolbox()->corePlugin()->preLoadFromJson(this, json); int version; if (!JsonHelper::validateExternalQGCJsonFile(json, kPlanFileType, kPlanFileVersion, kPlanFileVersion, version, errorString)) { qgcApp()->showAppMessage(errorMessage.arg(errorString)); return; } QList rgKeyInfo = { { kJsonMissionObjectKey, QJsonValue::Object, true }, { kJsonGeoFenceObjectKey, QJsonValue::Object, true }, { kJsonRallyPointsObjectKey, QJsonValue::Object, true }, }; if (!JsonHelper::validateKeys(json, rgKeyInfo, errorString)) { qgcApp()->showAppMessage(errorMessage.arg(errorString)); return; } if (!_missionController.load(json[kJsonMissionObjectKey].toObject(), errorString) || !_geoFenceController.load(json[kJsonGeoFenceObjectKey].toObject(), errorString) || !_rallyPointController.load(json[kJsonRallyPointsObjectKey].toObject(), errorString)) { qgcApp()->showAppMessage(errorMessage.arg(errorString)); } else { //-- Allow plugins to post process the load qgcApp()->toolbox()->corePlugin()->postLoadFromJson(this, json); success = true; } } else if (fileInfo.suffix() == AppSettings::missionFileExtension) { if (!_missionController.loadJsonFile(file, errorString)) { qgcApp()->showAppMessage(errorMessage.arg(errorString)); } else { success = true; } } else if (fileInfo.suffix() == AppSettings::waypointsFileExtension || fileInfo.suffix() == QStringLiteral("txt")) { if (!_missionController.loadTextFile(file, errorString)) { qgcApp()->showAppMessage(errorMessage.arg(errorString)); } else { success = true; } } else { //-- TODO: What then? } if(success){ _currentPlanFile = QString::asprintf("%s/%s.%s", fileInfo.path().toLocal8Bit().data(), fileInfo.completeBaseName().toLocal8Bit().data(), AppSettings::planFileExtension); } else { _currentPlanFile.clear(); } emit currentPlanFileChanged(); if (!offline()) { setDirty(true); } } QJsonDocument PlanMasterController::saveToJson() { QJsonObject planJson; qgcApp()->toolbox()->corePlugin()->preSaveToJson(this, planJson); QJsonObject missionJson; QJsonObject fenceJson; QJsonObject rallyJson; JsonHelper::saveQGCJsonFileHeader(planJson, kPlanFileType, kPlanFileVersion); //-- Allow plugin to preemptly add its own keys to mission qgcApp()->toolbox()->corePlugin()->preSaveToMissionJson(this, missionJson); _missionController.save(missionJson); //-- Allow plugin to add its own keys to mission qgcApp()->toolbox()->corePlugin()->postSaveToMissionJson(this, missionJson); _geoFenceController.save(fenceJson); _rallyPointController.save(rallyJson); planJson[kJsonMissionObjectKey] = missionJson; planJson[kJsonGeoFenceObjectKey] = fenceJson; planJson[kJsonRallyPointsObjectKey] = rallyJson; qgcApp()->toolbox()->corePlugin()->postSaveToJson(this, planJson); return QJsonDocument(planJson); } void PlanMasterController::saveToCurrent() { if(!_currentPlanFile.isEmpty()) { saveToFile(_currentPlanFile); } } void PlanMasterController::saveToFile(const QString& filename) { if (filename.isEmpty()) { return; } QString planFilename = filename; if (!QFileInfo(filename).fileName().contains(".")) { planFilename += QString(".%1").arg(fileExtension()); } QFile file(planFilename); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { qgcApp()->showAppMessage(tr("Plan save error %1 : %2").arg(filename).arg(file.errorString())); _currentPlanFile.clear(); emit currentPlanFileChanged(); } else { QJsonDocument saveDoc = saveToJson(); file.write(saveDoc.toJson()); if(_currentPlanFile != planFilename) { _currentPlanFile = planFilename; emit currentPlanFileChanged(); } } // Only clear dirty bit if we are offline if (offline()) { setDirty(false); } } void PlanMasterController::saveToKml(const QString& filename) { if (filename.isEmpty()) { return; } QString kmlFilename = filename; if (!QFileInfo(filename).fileName().contains(".")) { kmlFilename += QString(".%1").arg(kmlFileExtension()); } QFile file(kmlFilename); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { qgcApp()->showAppMessage(tr("KML save error %1 : %2").arg(filename).arg(file.errorString())); } else { KMLPlanDomDocument planKML; _missionController.addMissionToKML(planKML); QTextStream stream(&file); stream << planKML.toString(); file.close(); } } void PlanMasterController::removeAll(void) { _missionController.removeAll(); _geoFenceController.removeAll(); _rallyPointController.removeAll(); if (_offline) { _missionController.setDirty(false); _geoFenceController.setDirty(false); _rallyPointController.setDirty(false); _currentPlanFile.clear(); emit currentPlanFileChanged(); } } void PlanMasterController::removeAllFromVehicle(void) { if (!offline()) { _missionController.removeAllFromVehicle(); if (_geoFenceController.supported()) { _geoFenceController.removeAllFromVehicle(); } if (_rallyPointController.supported()) { _rallyPointController.removeAllFromVehicle(); } setDirty(false); } else { qWarning() << "PlanMasterController::removeAllFromVehicle called while offline"; } } bool PlanMasterController::containsItems(void) const { return _missionController.containsItems() || _geoFenceController.containsItems() || _rallyPointController.containsItems(); } bool PlanMasterController::dirty(void) const { return _missionController.dirty() || _geoFenceController.dirty() || _rallyPointController.dirty(); } void PlanMasterController::setDirty(bool dirty) { _missionController.setDirty(dirty); _geoFenceController.setDirty(dirty); _rallyPointController.setDirty(dirty); } QString PlanMasterController::fileExtension(void) const { return AppSettings::planFileExtension; } QString PlanMasterController::kmlFileExtension(void) const { return AppSettings::kmlFileExtension; } QStringList PlanMasterController::loadNameFilters(void) const { QStringList filters; filters << tr("Supported types (*.%1 *.%2 *.%3 *.%4)").arg(AppSettings::planFileExtension).arg(AppSettings::missionFileExtension).arg(AppSettings::waypointsFileExtension).arg("txt") << tr("All Files (*.*)"); return filters; } QStringList PlanMasterController::saveNameFilters(void) const { QStringList filters; filters << tr("Plan Files (*.%1)").arg(fileExtension()) << tr("All Files (*.*)"); return filters; } void PlanMasterController::sendPlanToVehicle(Vehicle* vehicle, const QString& filename) { // Use a transient PlanMasterController to accomplish this PlanMasterController* controller = new PlanMasterController(); controller->startStaticActiveVehicle(vehicle, true /* deleteWhenSendCompleted */); controller->loadFromFile(filename); controller->sendToVehicle(); } void PlanMasterController::_showPlanFromManagerVehicle(void) { if (!_managerVehicle->initialPlanRequestComplete() && !syncInProgress()) { // Something went wrong with initial load. All controllers are idle, so just force it off _managerVehicle->forceInitialPlanRequestComplete(); } // The crazy if structure is to handle the load propagating by itself through the system if (!_missionController.showPlanFromManagerVehicle()) { if (!_geoFenceController.showPlanFromManagerVehicle()) { _rallyPointController.showPlanFromManagerVehicle(); } } } bool PlanMasterController::syncInProgress(void) const { return _missionController.syncInProgress() || _geoFenceController.syncInProgress() || _rallyPointController.syncInProgress(); } bool PlanMasterController::isEmpty(void) const { return _missionController.isEmpty() && _geoFenceController.isEmpty() && _rallyPointController.isEmpty(); } void PlanMasterController::_updatePlanCreatorsList(void) { if (!_flyView) { if (!_planCreators) { _planCreators = new QmlObjectListModel(this); _planCreators->append(new BlankPlanCreator(this, this)); _planCreators->append(new SurveyPlanCreator(this, this)); _planCreators->append(new CorridorScanPlanCreator(this, this)); emit planCreatorsChanged(_planCreators); } if (_managerVehicle->fixedWing()) { if (_planCreators->count() == 4) { _planCreators->removeAt(_planCreators->count() - 1); } } else { if (_planCreators->count() != 4) { _planCreators->append(new StructureScanPlanCreator(this, this)); } } } }