QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
PlanMasterController.cc
Go to the documentation of this file.
2#include "AppMessages.h"
3#include "QGCCorePlugin.h"
5#include "Vehicle.h"
7#include "SettingsManager.h"
8#include "AppSettings.h"
9#include "JsonParsing.h"
10#include "MissionManager.h"
11#include "KMLPlanDomDocument.h"
12#include "PlanCreator.h"
13#include "QmlObjectListModel.h"
14#include "GeoFenceManager.h"
15#include "RallyPointManager.h"
16#include "QGCCompression.h"
17#include "QGCCompressionJob.h"
18#include "QGCLoggingCategory.h"
19
20#include <QtCore/QDir>
21#include <QtCore/QDirIterator>
22#include <QtCore/QFileInfo>
23#include <QtCore/QJsonDocument>
24#include <QtCore/QRegularExpression>
25
26QGC_LOGGING_CATEGORY(PlanMasterControllerLog, "PlanManager.PlanMasterController")
27
29 : QObject (parent)
30 , _multiVehicleMgr (MultiVehicleManager::instance())
31 , _controllerVehicle (new Vehicle(Vehicle::MAV_AUTOPILOT_TRACK, Vehicle::MAV_TYPE_TRACK, this))
32 , _managerVehicle (_controllerVehicle)
33 , _missionController (this)
34 , _geoFenceController (this)
35 , _rallyPointController (this)
36{
37 _commonInit();
38}
39
40#ifdef QGC_UNITTEST_BUILD
41PlanMasterController::PlanMasterController(MAV_AUTOPILOT firmwareType, MAV_TYPE vehicleType, QObject* parent)
42 : QObject (parent)
43 , _multiVehicleMgr (MultiVehicleManager::instance())
44 , _controllerVehicle (new Vehicle(firmwareType, vehicleType))
45 , _managerVehicle (_controllerVehicle)
46 , _missionController (this)
47 , _geoFenceController (this)
48 , _rallyPointController (this)
49{
50 _commonInit();
51}
52#endif
53
54void PlanMasterController::_commonInit(void)
55{
56 connect(&_missionController, &MissionController::dirtyChanged, this, &PlanMasterController::_updateOverallDirty);
57 connect(&_geoFenceController, &GeoFenceController::dirtyChanged, this, &PlanMasterController::_updateOverallDirty);
58 connect(&_rallyPointController, &RallyPointController::dirtyChanged, this, &PlanMasterController::_updateOverallDirty);
59
63
64 connect(this, &PlanMasterController::containsItemsChanged, this, &PlanMasterController::_updateShowCreateFromTemplate);
65
69
70 // Offline vehicle can change firmware/vehicle type
71 connect(_controllerVehicle, &Vehicle::vehicleTypeChanged, this, &PlanMasterController::_updatePlanCreatorsList);
72}
73
74
79
81{
82 _missionController.start (_flyView);
83 _geoFenceController.start (_flyView);
84 _rallyPointController.start (_flyView);
85
86 _activeVehicleChanged(_multiVehicleMgr->activeVehicle());
87 connect(_multiVehicleMgr, &MultiVehicleManager::activeVehicleChanged, this, &PlanMasterController::_activeVehicleChanged);
88
89 _updatePlanCreatorsList();
90}
91
92void PlanMasterController::startStaticActiveVehicle(Vehicle* vehicle, bool deleteWhenSendCompleted)
93{
94 _flyView = true;
95 _deleteWhenSendCompleted = deleteWhenSendCompleted;
96 _missionController.start(_flyView);
97 _geoFenceController.start(_flyView);
98 _rallyPointController.start(_flyView);
99 _activeVehicleChanged(vehicle);
100}
101
102void PlanMasterController::_activeVehicleChanged(Vehicle* activeVehicle)
103{
104 if (_managerVehicle == activeVehicle) {
105 // We are already setup for this vehicle
106 return;
107 }
108
109 qCDebug(PlanMasterControllerLog) << "_activeVehicleChanged" << activeVehicle;
110
111 if (_managerVehicle) {
112 // Disconnect old vehicle. Be careful of wildcarding disconnect too much since _managerVehicle may equal _controllerVehicle
113 disconnect(_managerVehicle->missionManager(), nullptr, this, nullptr);
114 disconnect(_managerVehicle->geoFenceManager(), nullptr, this, nullptr);
115 disconnect(_managerVehicle->rallyPointManager(), nullptr, this, nullptr);
116 }
117
118 bool newOffline = false;
119 if (activeVehicle == nullptr) {
120 // Since there is no longer an active vehicle we use the offline controller vehicle as the manager vehicle
121 _managerVehicle = _controllerVehicle;
122 newOffline = true;
123 } else {
124 newOffline = false;
125 _managerVehicle = activeVehicle;
126
127 // Update controllerVehicle to the currently connected vehicle
129 appSettings->offlineEditingFirmwareClass()->setRawValue(QGCMAVLink::firmwareClass(_managerVehicle->firmwareType()));
130 appSettings->offlineEditingVehicleClass()->setRawValue(QGCMAVLink::vehicleClass(_managerVehicle->vehicleType()));
131
132 // We use these signals to sequence upload and download to the multiple controller/managers
133 connect(_managerVehicle->missionManager(), &MissionManager::newMissionItemsAvailable, this, &PlanMasterController::_loadMissionComplete);
134 connect(_managerVehicle->geoFenceManager(), &GeoFenceManager::loadComplete, this, &PlanMasterController::_loadGeoFenceComplete);
135 connect(_managerVehicle->rallyPointManager(), &RallyPointManager::loadComplete, this, &PlanMasterController::_loadRallyPointsComplete);
136 connect(_managerVehicle->missionManager(), &MissionManager::sendComplete, this, &PlanMasterController::_sendMissionComplete);
137 connect(_managerVehicle->geoFenceManager(), &GeoFenceManager::sendComplete, this, &PlanMasterController::_sendGeoFenceComplete);
138 connect(_managerVehicle->rallyPointManager(), &RallyPointManager::sendComplete, this, &PlanMasterController::_sendRallyPointsComplete);
139 }
140
141 _offline = newOffline;
142 emit offlineChanged(offline());
143 emit managerVehicleChanged(_managerVehicle);
144
145 if (_flyView) {
146 // We are in the Fly View
147 if (newOffline) {
148 // No active vehicle, clear mission
149 qCDebug(PlanMasterControllerLog) << "_activeVehicleChanged: Fly View - No active vehicle, clearing stale plan";
150 removeAll();
151 } else {
152 // Fly view has changed to a new active vehicle, update to show correct mission
153 qCDebug(PlanMasterControllerLog) << "_activeVehicleChanged: Fly View - New active vehicle, loading new plan from manager vehicle";
154 _showPlanFromManagerVehicle();
155 }
156 } else {
157 // We are in the Plan view.
158 if (containsItems()) {
159 // We have a plan which is from a different vehicle than the new active vehicle. By definition this plan requires and upload.
160 _setDirtyForUpload(true);
161
162 // The plan view has a stale plan in it
163 if (dirtyForSave()) {
164 // Plan is dirty, the user must decide what to do in all cases
165 qCDebug(PlanMasterControllerLog) << "_activeVehicleChanged: Plan View - Previous dirty plan exists, no new active vehicle, sending promptForPlanUsageOnVehicleChange signal";
167 } else {
168 // Plan is not dirty
169 if (newOffline) {
170 // The active vehicle went away with no new active vehicle
171 qCDebug(PlanMasterControllerLog) << "_activeVehicleChanged: Plan View - Previous clean plan exists, no new active vehicle, clear stale plan";
172 removeAll();
173 } else {
174 // We are transitioning from one active vehicle to another. Show the plan from the new vehicle.
175 qCDebug(PlanMasterControllerLog) << "_activeVehicleChanged: Plan View - Previous clean plan exists, new active vehicle, loading from new manager vehicle";
176 _showPlanFromManagerVehicle();
177 }
178 }
179 } else {
180 // There is no previous Plan in the view
181 _setDirtyStates(false, false);
182 if (newOffline) {
183 // Nothing special to do in this case
184 qCDebug(PlanMasterControllerLog) << "_activeVehicleChanged: Plan View - No previous plan, no longer connected to vehicle, nothing to do";
185 } else {
186 // Just show the plan from the new vehicle
187 qCDebug(PlanMasterControllerLog) << "_activeVehicleChanged: Plan View - No previous plan, new active vehicle, loading from new manager vehicle";
188 _showPlanFromManagerVehicle();
189 }
190 }
191 }
192
193 // Vehicle changed so we need to signal everything
198
199 _updatePlanCreatorsList();
200}
201
203{
204 SharedLinkInterfacePtr sharedLink = _managerVehicle->vehicleLinkManager()->primaryLink().lock();
205 if (sharedLink) {
206 if (sharedLink->linkConfiguration()->isHighLatency()) {
207 QGC::showAppMessage(tr("Download not supported on high latency links."));
208 return;
209 }
210 } else {
211 // Vehicle is shutting down
212 return;
213 }
214
215 if (offline()) {
216 qCCritical(PlanMasterControllerLog) << "PlanMasterController::loadFromVehicle called while offline";
217 } else if (_flyView) {
218 qCCritical(PlanMasterControllerLog) << "PlanMasterController::loadFromVehicle called from Fly view";
219 } else if (syncInProgress()) {
220 qCCritical(PlanMasterControllerLog) << "PlanMasterController::loadFromVehicle called while syncInProgress";
221 } else {
222 _loadGeoFence = true;
223 qCDebug(PlanMasterControllerLog) << "PlanMasterController::loadFromVehicle calling _missionController.loadFromVehicle";
224 _missionController.loadFromVehicle();
225 }
226}
227
228
229void PlanMasterController::_loadMissionComplete(void)
230{
231 if (!_flyView && _loadGeoFence) {
232 _loadGeoFence = false;
233 _loadRallyPoints = true;
234 if (_geoFenceController.supported()) {
235 qCDebug(PlanMasterControllerLog) << "PlanMasterController::_loadMissionComplete calling _geoFenceController.loadFromVehicle";
236 _geoFenceController.loadFromVehicle();
237 } else {
238 qCDebug(PlanMasterControllerLog) << "PlanMasterController::_loadMissionComplete GeoFence not supported skipping";
239 _geoFenceController.removeAll();
240 _loadGeoFenceComplete();
241 }
242 }
243}
244
245void PlanMasterController::_loadGeoFenceComplete(void)
246{
247 if (!_flyView && _loadRallyPoints) {
248 _loadRallyPoints = false;
249 if (_rallyPointController.supported()) {
250 qCDebug(PlanMasterControllerLog) << "PlanMasterController::_loadGeoFenceComplete calling _rallyPointController.loadFromVehicle";
251 _rallyPointController.loadFromVehicle();
252 } else {
253 qCDebug(PlanMasterControllerLog) << "PlanMasterController::_loadMissionComplete Rally Points not supported skipping";
254 _rallyPointController.removeAll();
255 _loadRallyPointsComplete();
256 }
257 }
258}
259
260void PlanMasterController::_loadRallyPointsComplete(void)
261{
262 qCDebug(PlanMasterControllerLog) << "PlanMasterController::_loadRallyPointsComplete";
263 // A plan just downloaded from the vehicle reflects exactly what is on the vehicle.
264 // The user has made no edits, so it must not be dirty for save or upload.
265 _setDirtyStates(false /* dirtyForSave */, false /* dirtyForUpload */);
266}
267
268void PlanMasterController::_sendMissionComplete(void)
269{
270 if (_sendGeoFence) {
271 _sendGeoFence = false;
272 _sendRallyPoints = true;
273 if (_geoFenceController.supported()) {
274 qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle start GeoFence sendToVehicle";
275 _geoFenceController.sendToVehicle();
276 } else {
277 qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle GeoFence not supported skipping";
278 _sendGeoFenceComplete();
279 }
280 }
281}
282
283void PlanMasterController::_sendGeoFenceComplete(void)
284{
285 if (_sendRallyPoints) {
286 _sendRallyPoints = false;
287 if (_rallyPointController.supported()) {
288 qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle start rally sendToVehicle";
289 _rallyPointController.sendToVehicle();
290 } else {
291 qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle Rally Points not support skipping";
292 _sendRallyPointsComplete();
293 }
294 }
295}
296
297void PlanMasterController::_sendRallyPointsComplete(void)
298{
299 qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle Rally Point send complete";
300 _setDirtyForUpload(false);
301 if (_deleteWhenSendCompleted) {
302 this->deleteLater();
303 }
304}
305
307{
308 SharedLinkInterfacePtr sharedLink = _managerVehicle->vehicleLinkManager()->primaryLink().lock();
309 if (sharedLink) {
310 if (sharedLink->linkConfiguration()->isHighLatency()) {
311 QGC::showAppMessage(tr("Upload not supported on high latency links."));
312 return;
313 }
314 } else {
315 // Vehicle is shutting down
316 return;
317 }
318
319 if (offline()) {
320 qCCritical(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle called while offline";
321 } else if (syncInProgress()) {
322 qCCritical(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle called while syncInProgress";
323 } else {
324 qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle start mission sendToVehicle";
325 _sendGeoFence = true;
326 _missionController.sendToVehicle();
327 }
328}
329
330void PlanMasterController::loadFromFile(const QString& filename)
331{
332 QString errorString;
333 QString errorMessage = tr("Error loading Plan file (%1). %2").arg(filename).arg("%1");
334
335 if (filename.isEmpty()) {
336 return;
337 }
338
339 QFileInfo fileInfo(filename);
340 QFile file(filename);
341
342 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
343 errorString = file.errorString() + QStringLiteral(" ") + filename;
344 QGC::showAppMessage(errorMessage.arg(errorString));
345 return;
346 }
347
348 bool success = false;
349 if (fileInfo.suffix() == AppSettings::waypointsFileExtension || fileInfo.suffix() == QStringLiteral("txt")) {
350 if (!_missionController.loadTextFile(file, errorString)) {
351 QGC::showAppMessage(errorMessage.arg(errorString));
352 } else {
353 success = true;
354 }
355 } else {
356 QJsonDocument jsonDoc;
357 QByteArray bytes = file.readAll();
358
359 if (!JsonParsing::isJsonFile(bytes, jsonDoc, errorString)) {
360 QGC::showAppMessage(errorMessage.arg(errorString));
361 return;
362 }
363
364 QJsonObject json = jsonDoc.object();
365 //-- Allow plugins to pre process the load
367
368 int version;
370 QGC::showAppMessage(errorMessage.arg(errorString));
371 return;
372 }
373
374 QList<JsonParsing::KeyValidateInfo> rgKeyInfo = {
375 { kJsonMissionObjectKey, QJsonValue::Object, true },
376 { kJsonGeoFenceObjectKey, QJsonValue::Object, true },
377 { kJsonRallyPointsObjectKey, QJsonValue::Object, true },
378 };
379 if (!JsonParsing::validateKeys(json, rgKeyInfo, errorString)) {
380 QGC::showAppMessage(errorMessage.arg(errorString));
381 return;
382 }
383
384 if (!_missionController.load(json[kJsonMissionObjectKey].toObject(), errorString) ||
385 !_geoFenceController.load(json[kJsonGeoFenceObjectKey].toObject(), errorString) ||
386 !_rallyPointController.load(json[kJsonRallyPointsObjectKey].toObject(), errorString)) {
387 QGC::showAppMessage(errorMessage.arg(errorString));
388 } else {
389 //-- Allow plugins to post process the load
391 success = true;
392 }
393 }
394
395 if (success){
396 const bool oldRenamed = planFileRenamed();
397 _currentPlanFile = QString::asprintf("%s/%s.%s", fileInfo.path().toLocal8Bit().data(), fileInfo.completeBaseName().toLocal8Bit().data(), AppSettings::planFileExtension);
398 const bool currentNameChanged = (_currentPlanFileName != fileInfo.completeBaseName());
399 const bool originalNameChanged = (_originalPlanFileName != fileInfo.completeBaseName());
400 _currentPlanFileName = fileInfo.completeBaseName();
401 _originalPlanFileName = _currentPlanFileName;
402 _setDirtyStates(false /* dirtyForSave */, true /* dirtyForUpload */);
404 if (currentNameChanged) {
406 }
407 if (originalNameChanged) {
409 }
410 if (oldRenamed != planFileRenamed()) {
412 }
413 } else {
414 const bool hadFile = !_currentPlanFile.isEmpty();
415 const bool hadCurrentName = !_currentPlanFileName.isEmpty();
416 const bool hadOriginalName = !_originalPlanFileName.isEmpty();
417 const bool wasRenamed = planFileRenamed();
418 _currentPlanFile.clear();
419 _currentPlanFileName.clear();
420 _originalPlanFileName.clear();
421 if (hadFile) {
423 }
424 if (hadCurrentName) {
426 }
427 if (hadOriginalName) {
429 }
430 if (wasRenamed != planFileRenamed()) {
432 }
433 }
434}
435
437{
438 QJsonObject planJson;
439 QGCCorePlugin::instance()->preSaveToJson(this, planJson);
440 QJsonObject missionJson;
441 QJsonObject fenceJson;
442 QJsonObject rallyJson;
444 //-- Allow plugin to preemptly add its own keys to mission
445 QGCCorePlugin::instance()->preSaveToMissionJson(this, missionJson);
446 _missionController.save(missionJson);
447 //-- Allow plugin to add its own keys to mission
449 _geoFenceController.save(fenceJson);
450 _rallyPointController.save(rallyJson);
451 planJson[kJsonMissionObjectKey] = missionJson;
452 planJson[kJsonGeoFenceObjectKey] = fenceJson;
453 planJson[kJsonRallyPointsObjectKey] = rallyJson;
454 QGCCorePlugin::instance()->postSaveToJson(this, planJson);
455 return QJsonDocument(planJson);
456}
457
458bool
460{
461 if (!_currentPlanFile.isEmpty()) {
462 const bool saveSuccess = saveToFile(_currentPlanFile);
463 return saveSuccess;
464 }
465
466 return false;
467}
468
469bool PlanMasterController::saveToFile(const QString& filename)
470{
471 if (filename.isEmpty()) {
472 return false;
473 }
474
475 QString planFilename = filename;
476 if (!QFileInfo(filename).fileName().contains(".")) {
477 planFilename += QString(".%1").arg(fileExtension());
478 }
479
480 QFile file(planFilename);
481
482 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
483 QGC::showAppMessage(tr("Plan save error %1 : %2").arg(filename).arg(file.errorString()));
484 return false;
485 } else {
486 const QByteArray saveBytes = saveToJson().toJson();
487 const qint64 bytesWritten = file.write(saveBytes);
488 if (bytesWritten != saveBytes.size()) {
489 QGC::showAppMessage(tr("Plan save error %1 : %2").arg(filename).arg(file.errorString()));
490 return false;
491 }
492 if(_currentPlanFile != planFilename) {
493 _currentPlanFile = planFilename;
495 }
496 const bool wasRenamed = planFileRenamed();
497 const QString savedBaseName = QFileInfo(planFilename).completeBaseName();
498 if (_currentPlanFileName != savedBaseName) {
499 _currentPlanFileName = savedBaseName;
501 }
502 if (_originalPlanFileName != savedBaseName) {
503 _originalPlanFileName = savedBaseName;
505 }
506 if (wasRenamed != planFileRenamed()) {
508 }
509 _setDirtyForSave(false);
510 }
511
512 return true;
513}
514
515void PlanMasterController::saveToKml(const QString& filename)
516{
517 if (filename.isEmpty()) {
518 return;
519 }
520
521 QString kmlFilename = filename;
522 if (!QFileInfo(filename).fileName().contains(".")) {
523 kmlFilename += QString(".%1").arg(kmlFileExtension());
524 }
525
526 QFile file(kmlFilename);
527
528 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
529 QGC::showAppMessage(tr("KML save error %1 : %2").arg(filename).arg(file.errorString()));
530 } else {
531 KMLPlanDomDocument planKML;
532 _missionController.addMissionToKML(planKML);
533 QTextStream stream(&file);
534 stream << planKML.toString();
535 file.close();
536 }
537}
538
540{
541 _suppressOverallDirtyUpdate = true;
542 _missionController.removeAll();
543 _geoFenceController.removeAll();
544 _rallyPointController.removeAll();
545 _missionController.setDirty(false);
546 _geoFenceController.setDirty(false);
547 _rallyPointController.setDirty(false);
548 _suppressOverallDirtyUpdate = false;
549
550 _setDirtyStates(false, false);
551 if (_offline) {
552 _clearFileNames();
553 }
555}
556
558{
559 if (!offline()) {
560 _missionController.removeAllFromVehicle();
561 if (_geoFenceController.supported()) {
562 _geoFenceController.removeAllFromVehicle();
563 }
564 if (_rallyPointController.supported()) {
565 _rallyPointController.removeAllFromVehicle();
566 }
567 _setDirtyForUpload(false);
568 _clearFileNames();
569 } else {
570 qCCritical(PlanMasterControllerLog) << "PlanMasterController::removeAllFromVehicle called while offline";
571 }
573}
574
576{
577 return _missionController.containsItems() || _geoFenceController.containsItems() || _rallyPointController.containsItems();
578}
579
580void PlanMasterController::_updateShowCreateFromTemplate(void)
581{
582 // When the plan becomes empty, always return to template-selection mode regardless
583 // of how the items were removed.
584 if (!containsItems() && _userSelectedManualCreation) {
585 _userSelectedManualCreation = false;
587 }
588 const bool show = showCreateFromTemplate();
589 if (show != _showCreateFromTemplate) {
590 _showCreateFromTemplate = show;
592 }
593}
594
599
601{
602 // Normalize to a base name: trim whitespace, strip known extension, remove illegal characters
603 QString sanitized = name.trimmed();
604 const QString ext = QStringLiteral(".") + fileExtension();
605 if (sanitized.endsWith(ext, Qt::CaseInsensitive)) {
606 sanitized.chop(ext.length());
607 sanitized = sanitized.trimmed();
608 }
609 sanitized.remove(QRegularExpression(QStringLiteral("[/\\\\:*?\"<>|]")));
610 if (_currentPlanFileName != sanitized) {
611 const bool wasRenamed = planFileRenamed();
612 _currentPlanFileName = sanitized;
614 if (wasRenamed != planFileRenamed()) {
616 }
617 }
618}
619
621{
622 if (_currentPlanFileName.isEmpty()) {
623 return false;
624 }
625 return saveToFile(_resolvedPlanFilePath());
626}
627
629{
630 return !_originalPlanFileName.isEmpty() && _currentPlanFileName != _originalPlanFileName;
631}
632
634{
635 if (_currentPlanFileName.isEmpty()) {
636 return false;
637 }
638 return QFile::exists(_resolvedPlanFilePath());
639}
640
641QString PlanMasterController::_resolvedPlanFilePath() const
642{
643 const QString dir = _currentPlanFile.isEmpty()
645 : QFileInfo(_currentPlanFile).path();
646 return QStringLiteral("%1/%2.%3").arg(dir, _currentPlanFileName, fileExtension());
647}
648
649void PlanMasterController::_clearFileNames()
650{
651 const bool hadFile = !_currentPlanFile.isEmpty();
652 const bool hadCurrentName = !_currentPlanFileName.isEmpty();
653 const bool hadOriginalName = !_originalPlanFileName.isEmpty();
654 const bool wasRenamed = planFileRenamed();
655 _currentPlanFile.clear();
656 _currentPlanFileName.clear();
657 _originalPlanFileName.clear();
658 if (hadFile) {
660 }
661 if (hadCurrentName) {
663 }
664 if (hadOriginalName) {
666 }
667 if (wasRenamed != planFileRenamed()) {
669 }
670}
671
676
678{
679 QStringList filters;
680
681 filters << tr("Supported types (*.%1 *.%2 *.%3)").arg(AppSettings::planFileExtension).arg(AppSettings::waypointsFileExtension).arg("txt") <<
682 tr("All Files (*)");
683 return filters;
684}
685
686
688{
689 QStringList filters;
690
691 filters << tr("Plan Files (*.%1)").arg(fileExtension()) << tr("All Files (*)");
692 return filters;
693}
694
695void PlanMasterController::sendPlanToVehicle(Vehicle* vehicle, const QString& filename)
696{
697 // Use a transient PlanMasterController to accomplish this
698 PlanMasterController* controller = new PlanMasterController();
699 controller->startStaticActiveVehicle(vehicle, true /* deleteWhenSendCompleted */);
700 controller->loadFromFile(filename);
701 controller->sendToVehicle();
702}
703
704void PlanMasterController::_showPlanFromManagerVehicle(void)
705{
706 if (!_managerVehicle->initialPlanRequestComplete()) {
707 // We need to wait until initial load is complete before we show anything.
708 return;
709 }
710
711 // The crazy if structure is to handle the load propagating by itself through the system
712 if (!_missionController.showPlanFromManagerVehicle()) {
713 if (!_geoFenceController.showPlanFromManagerVehicle()) {
714 _rallyPointController.showPlanFromManagerVehicle();
715 }
716 }
717
718 // Showing the vehicle plan should leave both dirty states clean.
719 _missionController.setDirty(false);
720 _geoFenceController.setDirty(false);
721 _rallyPointController.setDirty(false);
722 _setDirtyStates(false, false);
723}
724
726{
727 return _missionController.syncInProgress() ||
728 _geoFenceController.syncInProgress() ||
729 _rallyPointController.syncInProgress();
730}
731
733{
734 return _missionController.isEmpty() &&
735 _geoFenceController.isEmpty() &&
736 _rallyPointController.isEmpty();
737}
738
739void PlanMasterController::_updateOverallDirty(void)
740{
741 if (syncInProgress() || _suppressOverallDirtyUpdate) {
742 return;
743 }
744
745 const bool saveDirty = _missionController.dirty() || _geoFenceController.dirty() || _rallyPointController.dirty();
746 if (saveDirty) {
747 _setDirtyForSave(true);
748 }
749}
750
751void PlanMasterController::_setDirtyForSave(bool dirtyForSave)
752{
753 if (_dirtyForSave != dirtyForSave) {
754 _dirtyForSave = dirtyForSave;
755 emit dirtyForSaveChanged(_dirtyForSave);
756
757 if (_dirtyForSave) {
758 _setDirtyForUpload(true);
759 }
760 }
761}
762
763void PlanMasterController::_setDirtyForUpload(bool dirtyForUpload)
764{
765 if (_dirtyForUpload != dirtyForUpload) {
766 _dirtyForUpload = dirtyForUpload;
767 emit dirtyForUploadChanged(_dirtyForUpload);
768 }
769}
770
771void PlanMasterController::_setDirtyStates(bool dirtyForSave, bool dirtyForUpload)
772{
773 const bool saveChanged = (_dirtyForSave != dirtyForSave);
774 const bool uploadChanged = (_dirtyForUpload != dirtyForUpload);
775
776 _dirtyForSave = dirtyForSave;
777 _dirtyForUpload = dirtyForUpload;
778
779 if (saveChanged) {
780 emit dirtyForSaveChanged(_dirtyForSave);
781 }
782 if (uploadChanged) {
783 emit dirtyForUploadChanged(_dirtyForUpload);
784 }
785}
786
787void PlanMasterController::_updatePlanCreatorsList(void)
788{
789 if (_flyView) {
790 return;
791 }
792
793 const auto vehicleClass = _managerVehicle->vehicleClass();
794
795 // Only rebuild if the vehicle class actually changed
796 if (_planCreators && _planCreatorsVehicleClass == vehicleClass) {
797 return;
798 }
799
800 if (!_planCreators) {
801 _planCreators = new QmlObjectListModel(this);
802 } else {
803 _planCreators->clearAndDeleteContents();
804 }
805
806 _planCreatorsVehicleClass = vehicleClass;
807
808 // Allow custom builds to provide their own list of plan creators
809 const QList<PlanCreator*> creators = QGCCorePlugin::instance()->planCreators(this);
810
811 // Filter by vehicle class and add to the model
812 for (PlanCreator* creator : creators) {
813 if (creator->supportsVehicleClass(vehicleClass)) {
814 _planCreators->append(creator);
815 } else {
816 delete creator;
817 }
818 }
819
820 emit planCreatorsChanged(_planCreators);
821}
822
824{
825 if (offline()) {
826 // There is no new vehicle so clear any previous plan
827 qCDebug(PlanMasterControllerLog) << "showPlanFromManagerVehicle: Plan View - No new vehicle, clear any previous plan";
828 removeAll();
829 } else {
830 // We have a new active vehicle, show the plan from that
831 qCDebug(PlanMasterControllerLog) << "showPlanFromManagerVehicle: Plan View - New vehicle available, show plan from new manager vehicle";
832 _showPlanFromManagerVehicle();
833 }
834}
835
836void PlanMasterController::setUserSelectedManualCreation(bool userSelectedManualCreation)
837{
838 if (_userSelectedManualCreation != userSelectedManualCreation) {
839 _userSelectedManualCreation = userSelectedManualCreation;
841 // Update showCreateFromTemplate directly — do not go through _updateShowCreateFromTemplate,
842 // which would immediately auto-clear the flag if the plan happens to be empty right now.
843 const bool show = showCreateFromTemplate();
844 if (show != _showCreateFromTemplate) {
845 _showCreateFromTemplate = show;
847 }
848 }
849}
850
851void PlanMasterController::loadFromArchive(const QString& archivePath)
852{
853 if (archivePath.isEmpty()) {
854 return;
855 }
856
857 if (!QFile::exists(archivePath)) {
858 QGC::showAppMessage(tr("Archive file not found: %1").arg(archivePath));
859 return;
860 }
861
862 if (!QGCCompression::isArchiveFile(archivePath)) {
863 QGC::showAppMessage(tr("Not a supported archive format: %1").arg(archivePath));
864 return;
865 }
866
867 const QString tempPath = QDir::temp().filePath(QStringLiteral("qgc_plan_") + QString::number(QDateTime::currentMSecsSinceEpoch()));
868 if (!QDir().mkpath(tempPath)) {
869 QGC::showAppMessage(tr("Could not create temporary directory"));
870 return;
871 }
872
873 _extractionOutputDir = tempPath;
874
875 if (_extractionJob == nullptr) {
876 _extractionJob = new QGCCompressionJob(this);
877 connect(_extractionJob, &QGCCompressionJob::finished,
878 this, &PlanMasterController::_handleExtractionFinished);
879 }
880
881 _extractionJob->extractArchive(archivePath, tempPath);
882}
883
884void PlanMasterController::_handleExtractionFinished(bool success)
885{
886 if (!success) {
887 const QString error = _extractionJob != nullptr ? _extractionJob->errorString() : tr("Extraction failed");
888 QGC::showAppMessage(tr("Failed to extract plan archive: %1").arg(error));
889 QDir(_extractionOutputDir).removeRecursively();
890 _extractionOutputDir.clear();
891 return;
892 }
893
894 QString planPath;
895 const QString planExt = QStringLiteral("*.") + AppSettings::planFileExtension;
896 QDirIterator it(_extractionOutputDir, {planExt}, QDir::Files, QDirIterator::Subdirectories);
897 if (it.hasNext()) {
898 planPath = it.next();
899 }
900
901 if (planPath.isEmpty()) {
902 QGC::showAppMessage(tr("No plan file found in archive"));
903 QDir(_extractionOutputDir).removeRecursively();
904 _extractionOutputDir.clear();
905 return;
906 }
907
908 qCDebug(PlanMasterControllerLog) << "Found plan file in archive:" << planPath;
909 loadFromFile(planPath);
910
911 QDir(_extractionOutputDir).removeRecursively();
912 _extractionOutputDir.clear();
913}
std::shared_ptr< LinkInterface > SharedLinkInterfacePtr
QObject wrapper for async compression operations using QtConcurrent/QPromise.
QString errorString
Error error
#define QGC_LOGGING_CATEGORY(name, categoryStr)
Application Settings.
Definition AppSettings.h:10
static constexpr const char * planFileExtension
static constexpr const char * kmlFileExtension
static constexpr const char * waypointsFileExtension
QString missionSavePath()
bool showPlanFromManagerVehicle(void) final
void removeAllFromVehicle(void) final
void setDirty(bool dirty) final
bool containsItems(void) const final
void sendToVehicle(void) final
bool syncInProgress(void) const final
void removeAll(void) final
Removes all from controller only.
bool load(const QJsonObject &json, QString &errorString) final
bool supported(void) const final
true: controller is waiting for the current load to complete
void loadFromVehicle(void) final
bool isEmpty(void) const
bool dirty(void) const final
void start(bool flyView) final
Should be called immediately upon Component.onCompleted.
void save(QJsonObject &json) final
void loadComplete(void)
void sendComplete(bool error)
Used to convert a Plan to a KML document.
bool showPlanFromManagerVehicle(void) final
bool dirty(void) const final
bool loadTextFile(QFile &file, QString &errorString)
void removeAllFromVehicle(void) final
bool load(const QJsonObject &json, QString &errorString) final
void addMissionToKML(KMLPlanDomDocument &planKML)
bool syncInProgress(void) const final
void sendToVehicle(void) final
bool isEmpty(void) const
void save(QJsonObject &json) final
bool containsItems(void) const final
void removeAll(void) final
Removes all from controller only.
void setDirty(bool dirty) final
void start(bool flyView) final
Should be called immediately upon Component.onCompleted.
void loadFromVehicle(void) final
Vehicle * activeVehicle() const
void activeVehicleChanged(Vehicle *activeVehicle)
Base class for PlanCreator objects which are used to create a full plan in a single step.
Definition PlanCreator.h:16
void syncInProgressChanged(bool syncInProgress)
void dirtyChanged(bool dirty)
void sendComplete(bool error)
void newMissionItemsAvailable(bool removeAllRequested)
Master controller for mission, fence, rally.
Q_INVOKABLE void removeAll(void)
Removes all from controller only, sync required to remove from vehicle.
PlanMasterController(QObject *parent=nullptr)
void planFileRenamedChanged(void)
Q_INVOKABLE bool saveWithCurrentName()
Save using the (possibly renamed) currentPlanFileName.
void setCurrentPlanFileName(const QString &name)
Q_INVOKABLE bool saveToCurrent()
void originalPlanFileNameChanged(void)
QStringList saveNameFilters(void) const
void currentPlanFileNameChanged(void)
void promptForPlanUsageOnVehicleChange(void)
void showCreateFromTemplateChanged()
Q_INVOKABLE void saveToKml(const QString &filename)
void setUserSelectedManualCreation(bool userSelectedManualCreation)
Q_INVOKABLE void showPlanFromManagerVehicle(void)
Replaces any current plan with the plan from the manager vehicle even if offline.
static constexpr int kPlanFileVersion
Q_INVOKABLE void start(void)
Should be called immediately upon Component.onCompleted.
QStringList loadNameFilters(void) const
void planCreatorsChanged(QmlObjectListModel *planCreators)
Q_INVOKABLE void loadFromVehicle(void)
void managerVehicleChanged(Vehicle *managerVehicle)
QString kmlFileExtension(void) const
void syncInProgressChanged(void)
Q_INVOKABLE void loadFromFile(const QString &filename)
Q_INVOKABLE void startStaticActiveVehicle(Vehicle *vehicle, bool deleteWhenSendCompleted=false)
bool dirtyForUpload(void) const
static constexpr const char * kJsonGeoFenceObjectKey
Q_INVOKABLE void loadFromArchive(const QString &archivePath)
void dirtyForSaveChanged(bool dirtyForSave)
void userSelectedManualCreationChanged()
bool syncInProgress(void) const
void currentPlanFileChanged(void)
bool showCreateFromTemplate(void) const
bool dirtyForSave(void) const
QString fileExtension(void) const
~PlanMasterController()
Either active vehicle or _controllerVehicle if no active vehicle.
Q_INVOKABLE bool saveToFile(const QString &filename)
bool planFileRenamed(void) const
Q_INVOKABLE bool resolvedPlanFileExists() const
true if a file at the renamed path already exists on disk
void dirtyForUploadChanged(bool dirtyForUpload)
static constexpr const char * kPlanFileType
static constexpr const char * kJsonMissionObjectKey
Q_INVOKABLE void removeAllFromVehicle(void)
Removes all from vehicle and controller.
static void sendPlanToVehicle(Vehicle *vehicle, const QString &filename)
void offlineChanged(bool offlineEditing)
Q_INVOKABLE void sendToVehicle(void)
bool userSelectedManualCreation(void) const
static constexpr const char * kJsonRallyPointsObjectKey
QObject wrapper for compression operations with progress signals.
QString errorString() const
void finished(bool success)
void extractArchive(const QString &archivePath, const QString &outputDirectoryPath, qint64 maxBytes=0)
virtual void postSaveToMissionJson(PlanMasterController *pController, QJsonObject &missionJson)
Allows custom builds to add custom items to the mission section of the plan file after the item is cr...
virtual void preLoadFromJson(PlanMasterController *pController, QJsonObject &json)
Allows custom builds to load custom items from the plan file before the document is parsed.
virtual void preSaveToMissionJson(PlanMasterController *pController, QJsonObject &missionJson)
Allows custom builds to add custom items to the mission section of the plan file before the item is c...
virtual void preSaveToJson(PlanMasterController *pController, QJsonObject &json)
Allows custom builds to add custom items to the plan file before the document is created.
virtual void postSaveToJson(PlanMasterController *pController, QJsonObject &json)
Allows custom builds to add custom items to the plan file after the document is created.
virtual QList< PlanCreator * > planCreators(PlanMasterController *planMasterController)
static QGCCorePlugin * instance()
virtual void postLoadFromJson(PlanMasterController *pController, QJsonObject &json)
Allows custom builds to load custom items from the plan file after the document is parsed.
void append(QObject *object)
Caller maintains responsibility for object ownership and deletion.
void clearAndDeleteContents() override final
Clears the list and calls deleteLater on each entry.
bool containsItems(void) const final
void save(QJsonObject &json) final
bool supported(void) const final
true: controller is waiting for the current load to complete
void setDirty(bool dirty) final
bool showPlanFromManagerVehicle(void) final
bool syncInProgress(void) const final
bool dirty(void) const final
void start(bool flyView) final
Should be called immediately upon Component.onCompleted.
void removeAllFromVehicle(void) final
bool load(const QJsonObject &json, QString &errorString) final
void removeAll(void) final
Removes all from controller only.
void loadFromVehicle(void) final
void loadComplete(void)
void sendComplete(bool error)
static SettingsManager * instance()
AppSettings * appSettings() const
WeakLinkInterfacePtr primaryLink() const
QGCMAVLink::VehicleClass_t vehicleClass(void) const
Definition Vehicle.h:433
MAV_TYPE vehicleType() const
Definition Vehicle.h:432
VehicleLinkManager * vehicleLinkManager()
Definition Vehicle.h:579
MAV_AUTOPILOT firmwareType() const
Definition Vehicle.h:431
GeoFenceManager * geoFenceManager()
Definition Vehicle.h:575
RallyPointManager * rallyPointManager()
Definition Vehicle.h:576
bool initialPlanRequestComplete() const
Definition Vehicle.h:715
MissionManager * missionManager()
Definition Vehicle.h:574
void vehicleTypeChanged()
bool validateExternalQGCJsonFile(const QJsonObject &jsonObject, const QString &expectedFileType, int minSupportedVersion, int maxSupportedVersion, int &version, QString &errorString)
bool validateKeys(const QJsonObject &jsonObject, const QList< KeyValidateInfo > &keyInfo, QString &errorString)
Validates that all required keys are present and that listed keys have the expected type.
bool isJsonFile(const QByteArray &bytes, QJsonDocument &jsonDoc, QString &errorString)
Determines whether an in-memory byte buffer contains parseable JSON content.
void saveQGCJsonFileHeader(QJsonObject &jsonObject, const QString &fileType, int version)
Saves the standard QGC file header (groundStation, fileType, version) into the json object.
bool isArchiveFile(const QString &filePath)
Check if file path indicates an archive file (.zip, .tar, .tar.gz, etc.)
void showAppMessage(const QString &message, const QString &title)
Modal application message. Queued if the UI isn't ready yet.
Definition AppMessages.cc:9