QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
PositionManager.cc
Go to the documentation of this file.
1#include "PositionManager.h"
2
3#include <utility>
4
5#include <QtCore/QCoreApplication>
6#include <QtCore/QPermissions>
7#include <QtCore/QSignalBlocker>
8
10#include "RuntimeScheduler.h"
11
12QGC_LOGGING_CATEGORY(PositionManagerLog, "GPS.PositionManager.QGCPositionManager")
13
14namespace {
15
17constexpr GPSAltitudeDatum platformAltitudeDatum()
18{
19#if defined(Q_OS_ANDROID)
20 // The Android backend reports WGS84 ellipsoid height while its useMslAltitude parameter is false.
22#elif defined(Q_OS_DARWIN)
23 // Core Location reports altitude above mean sea level.
25#else
27#endif
28}
29
30} // namespace
31
33 : QObject(parent)
34 , _scheduler(RuntimeScheduler::orDefault(scheduler, this))
35 , _recoveryTask(_scheduler, this)
36 , _deviceHealth(new GPSSourceHealth(this, _scheduler))
37{
38 qCDebug(PositionManagerLog) << this;
39 connect(_deviceHealth, &GPSSourceHealth::positionChanged, this, [this]() { _update(SelectedSource::Internal); });
40}
41
43{
44 qCDebug(PositionManagerLog) << "Position manager shutdown:" << this;
45 blockSignals(true);
46 // Stop the borrowed backend and drop the receiver binding while the manager is still whole.
47 _releaseDevice();
48 if (_receiver) {
49 _receiver->disconnect(this);
50 }
51}
52
54{
56 if (_shutdown || _configuration == configuration ||
58 return;
59 }
60 _configuration = configuration;
61 _receiverUsableSinceMs.reset();
62 _recoveryTask.cancel();
63 _reselect = true;
64 _update();
65 _setDeviceActive(mode != SourceMode::ReceiverOnly);
66}
67
69{
70 _platformSourceFactory = std::move(factory);
71}
72
74{
75 if (_shutdown) {
76 return;
77 }
78 _checkPermission();
79}
80
82{
83 if (std::exchange(_shutdown, true)) {
84 return;
85 }
86 qCDebug(PositionManagerLog) << "Releasing position sources";
88}
89
90void PositionManager::_setupPositionSources()
91{
92 auto* platformSource = _platformSourceFactory ? _platformSourceFactory(this) : nullptr;
93 const bool custom = platformSource != nullptr;
94 if (!custom) {
95 // Android reports MSL altitude only when asked; platformAltitudeDatum() relies on ellipsoid height.
96 platformSource = QGeoPositionInfoSource::createDefaultSource({{QStringLiteral("useMslAltitude"), false}}, this);
97 if (platformSource) {
98 qCDebug(PositionManagerLog) << "Device position backend:" << platformSource->sourceName();
99 }
100 }
101 setInternalPositionSource(platformSource,
103 custom ? GPSAltitudeDatum::Unknown : platformAltitudeDatum());
104}
105
106void PositionManager::_handlePermissionStatus(Qt::PermissionStatus permissionStatus)
107{
108 if (_shutdown) {
109 return;
110 }
111 if (permissionStatus == Qt::PermissionStatus::Granted) {
112 _setupPositionSources();
113 } else {
114 _setInternalPositionStatus(SourceStatus::PermissionDenied);
115 }
116}
117
118void PositionManager::_checkPermission()
119{
120 QLocationPermission locationPermission;
121 locationPermission.setAccuracy(QLocationPermission::Precise);
122 const auto status = QCoreApplication::instance()->checkPermission(locationPermission);
123 if (status == Qt::PermissionStatus::Undetermined) {
124 _setInternalPositionStatus(SourceStatus::PermissionRequired);
125 QCoreApplication::instance()->requestPermission(
126 locationPermission, this,
127 [this](const QPermission& permission) { _handlePermissionStatus(permission.status()); });
128 } else {
129 _handlePermissionStatus(status);
130 }
131}
132
133void PositionManager::setInternalPositionSource(QGeoPositionInfoSource* source, SourceStatus status, bool custom,
134 GPSAltitudeDatum altitudeDatum)
135{
136 _usingPluginSource = custom;
137 _deviceAltitudeDatum = altitudeDatum;
138 _platformStatus = status;
139 if (source == _device) {
140 _update();
141 return;
142 }
143 _releaseDevice();
144 _device = source;
145 if (source) {
146 connect(source, &QGeoPositionInfoSource::positionUpdated, this, &PositionManager::_devicePositionUpdated);
147 connect(source, &QGeoPositionInfoSource::errorOccurred, this, &PositionManager::_deviceErrorOccurred);
148 connect(source, &QObject::destroyed, this, [this]() {
149 _releaseDevice();
150 _bindingChanged(SelectedSource::Internal);
151 });
152 }
153 _bindingChanged(SelectedSource::Internal);
154 _setDeviceActive(_configuration.sourceMode != SourceMode::ReceiverOnly);
155}
156
157void PositionManager::_setInternalPositionStatus(SourceStatus status)
158{
159 _platformStatus = status;
160 _update();
161}
162
164{
165 if (!producer) {
166 return {};
167 }
168 _setReceiver(producer, sessionId);
169 // The handle points at this manager only so that it tests true; releasing it retires this binding alone.
171 this, [manager = QPointer<PositionManager>(this), token = _receiverToken](const void*) {
172 if (manager) {
173 manager->_retireReceiver(token);
174 }
175 });
176}
177
178void PositionManager::_setReceiver(GPSSourceHealth* producer, quint64 sessionId)
179{
180 if (_receiver) {
181 _receiver->disconnect(this);
182 }
183 _receiver = producer;
184 _receiverSession = sessionId;
185 ++_receiverToken;
186 if (producer) {
187 connect(producer, &GPSSourceHealth::positionChanged, this, [this]() { _update(SelectedSource::Receiver); });
188 connect(producer, &QObject::destroyed, this, [this]() { _setReceiver(nullptr, 0); });
189 }
190 _bindingChanged(SelectedSource::Receiver);
191}
192
193void PositionManager::_retireReceiver(quint64 token)
194{
195 if (token == _receiverToken) {
196 _setReceiver(nullptr, 0);
197 }
198}
199
200void PositionManager::_releaseDevice()
201{
202 _setDeviceActive(false);
203 if (_device) {
204 _device->disconnect(this);
205 }
206 _device = nullptr;
207}
208
209void PositionManager::_setDeviceActive(bool active)
210{
211 active = active && _device;
212 if (_deviceActive == active) {
213 return;
214 }
215 _deviceActive = active;
216 if (active) {
217 _device->setPreferredPositioningMethods(QGeoPositionInfoSource::SatellitePositioningMethods);
218#if !defined(Q_OS_DARWIN)
219 _device->setUpdateInterval(_device->minimumUpdateInterval());
220#endif
221 _device->startUpdates();
222 return;
223 }
224 if (_device) {
225 _device->stopUpdates();
226 }
227 // Reactivation must not publish a fix from before the stop. An inactive device is never consulted, so this
228 // reset needs no re-evaluation, and it must not notify while the service is being destroyed.
229 const QSignalBlocker silent(_deviceHealth);
230 _deviceHealth->reset();
231}
232
233void PositionManager::_devicePositionUpdated(const QGeoPositionInfo& position)
234{
235 // A stopped backend may still deliver a report it queued before stopUpdates().
236 if (!_deviceActive) {
237 return;
238 }
239 _platformStatus = SourceStatus::WaitingForFix;
240 if (_selectedKind == SelectedSource::Internal) {
241 _setPositioningError(QGeoPositionInfoSource::NoError);
242 }
243 GPSObservation observation;
244 observation.position = position;
245 observation.monotonicTimestampUs = _scheduler->nowUs();
246 // An altitude without a vertical accuracy may be a placeholder, such as Core Location's for a 2D fix.
247 if (position.hasAttribute(QGeoPositionInfo::VerticalAccuracy) && qIsFinite(position.coordinate().altitude())) {
248 observation.altitudeDatum = _deviceAltitudeDatum;
249 }
250 _deviceHealth->updateObservation(observation);
251}
252
253void PositionManager::_deviceErrorOccurred(QGeoPositionInfoSource::Error error)
254{
255 if (!_deviceActive) {
256 return;
257 }
258 if (error == QGeoPositionInfoSource::AccessError) {
259 _platformStatus = SourceStatus::PermissionDenied;
260 } else if (error == QGeoPositionInfoSource::ClosedError || error == QGeoPositionInfoSource::UnknownSourceError) {
261 _platformStatus = SourceStatus::BackendUnavailable;
262 }
263 if (_selectedKind == SelectedSource::Internal) {
264 _setPositioningError(error);
265 }
266 // A timeout means no new fix, which freshness already expires; only real failures invalidate the fix.
267 if (error != QGeoPositionInfoSource::NoError && error != QGeoPositionInfoSource::UpdateTimeoutError) {
268 _deviceHealth->invalidatePosition();
269 }
270 _update();
271}
272
273void PositionManager::_bindingChanged(SelectedSource kind)
274{
275 // A rebound role needs a fresh selection even when its health object is unchanged.
276 _reselect = _reselect || _selectedKind == kind;
277 _update();
278}
279
280void PositionManager::_update(SelectedSource reporter)
281{
282 const SelectedSource chosen = _choose();
283 GPSSourceHealth* const health = _sourceHealth(chosen);
284 bool republish = reporter == chosen;
285 if (_reselect || chosen != _selectedKind || _currentHealth != health) {
286 qCDebug(PositionManagerLog) << "Ground-station position source changed"
287 << "source:" << chosen << "selected:" << health;
288 _reselect = false;
289 _selectedKind = chosen;
290 _currentHealth = health;
291 _selectionRevision = health ? health->observationRevision() : 0;
292 _authorized = false;
293 _published = {};
294 _gcsPositioningError = QGeoPositionInfoSource::NoError;
295 republish = _configuration.sourceMode == SourceMode::Automatic;
296 }
297 // Standby deadlines can run before the selected source's deadline.
298 if (republish || (_published.position.isValid() && !_accepted(_selectedKind))) {
299 _republish();
300 }
301 _updateStatus();
302 _emitChanges();
303}
304
305PositionManager::SelectedSource PositionManager::_choose()
306{
307 if (_configuration.sourceMode == SourceMode::ReceiverOnly) {
309 }
310 if (_configuration.sourceMode == SourceMode::InternalOnly) {
312 }
313 const bool receiverUsable = _accepted(SelectedSource::Receiver).has_value();
314 const bool deviceUsable = _accepted(SelectedSource::Internal).has_value();
315 // A receiver that recovers while the device works takes over only after staying usable for RECOVERY_DELAY.
316 if (receiverUsable && deviceUsable && _selectedKind == SelectedSource::Internal) {
317 const qint64 nowMs = _scheduler->nowMs();
318 if (!_receiverUsableSinceMs) {
319 _receiverUsableSinceMs = nowMs;
320 }
321 const auto remaining = RECOVERY_DELAY - std::chrono::milliseconds(nowMs - *_receiverUsableSinceMs);
322 if (remaining > std::chrono::milliseconds::zero()) {
323 _recoveryTask.schedule(remaining, [this]() { _update(); });
325 }
326 }
327 _receiverUsableSinceMs.reset();
328 _recoveryTask.cancel();
329 if (receiverUsable) {
331 }
332 if (deviceUsable) {
334 }
335 if (_sourceHealth(_selectedKind)) {
336 return _selectedKind;
337 }
339}
340
341void PositionManager::_republish()
342{
343 _published = {};
344 if (!_currentHealth) {
345 return;
346 }
347 // Pinned selections need a new observation, not a timeout change.
348 _authorized = _configuration.sourceMode == SourceMode::Automatic ||
349 _currentHealth->observationRevision() != _selectionRevision;
350 const auto accepted = acceptedObservation();
351 if (!accepted) {
352 if (_currentHealth->state() != GPSSourceHealth::State::NoData &&
353 _gcsPositioningError == QGeoPositionInfoSource::NoError) {
354 _setPositioningError(QGeoPositionInfoSource::UpdateTimeoutError);
355 }
356 return;
357 }
358 _setPositioningError(QGeoPositionInfoSource::NoError);
359 _published = {.position = accepted->position.coordinate(),
360 .heading = accepted->heading(),
361 .horizontalAccuracy = accepted->position.attribute(QGeoPositionInfo::HorizontalAccuracy)};
362}
363
364void PositionManager::_updateStatus()
365{
366 _selectedSource = _currentHealth ? _selectedKind : SelectedSource::None;
368 if (_currentHealth) {
369 switch (_currentHealth->state()) {
372 break;
374 status = !_accepted(_selectedKind) ? SourceStatus::Stale
375 : _published.position.isValid() ? SourceStatus::Active
377 break;
379 status = SourceStatus::Stale;
380 break;
383 break;
384 }
385 } else if (_configuration.sourceMode != SourceMode::ReceiverOnly) {
386 status = _platformStatus;
387 }
388 if (_selectedSource == SelectedSource::Internal &&
389 (_platformStatus == SourceStatus::PermissionDenied || _platformStatus == SourceStatus::BackendUnavailable)) {
390 status = _platformStatus;
391 }
392 _sourceStatus = status;
393}
394
395void PositionManager::_emitChanges()
396{
397 // Each notified value is recorded before its signal, so a nested update never repeats an older value.
398 const Notified::Selection selection{_selectedSource, _sourceStatus, selectedSourceName()};
399 if (std::exchange(_notified.selection, selection) != selection) {
400 emit selectionChanged();
401 }
402 const qreal accuracy = _published.horizontalAccuracy;
403 if (std::exchange(_notified.publication.horizontalAccuracy, accuracy) != accuracy) {
405 }
406 const qreal heading = _published.heading;
407 const qreal notifiedHeading = std::exchange(_notified.publication.heading, heading);
408 if (notifiedHeading != heading && !(qIsNaN(notifiedHeading) && qIsNaN(heading))) {
409 emit gcsHeadingChanged(heading);
410 }
411 const QGeoCoordinate position = _published.position;
412 if (std::exchange(_notified.publication.position, position) != position) {
413 emit gcsPositionChanged(position);
414 }
415}
416
417void PositionManager::_setPositioningError(QGeoPositionInfoSource::Error error)
418{
419 if (_gcsPositioningError == error) {
420 return;
421 }
422 _gcsPositioningError = error;
423 if (error != QGeoPositionInfoSource::NoError) {
424 qCDebug(PositionManagerLog) << "Positioning error:" << error;
425 }
426}
427
428std::optional<GPSObservation> PositionManager::acceptedObservation(
429 GPSObservation::PositionUse use, std::optional<std::chrono::milliseconds> maximumAge) const
430{
431 return _currentHealth && _authorized ? _accepted(_selectedKind, use, maximumAge) : std::nullopt;
432}
433
434std::optional<GPSObservation> PositionManager::_accepted(SelectedSource kind, GPSObservation::PositionUse use,
435 std::optional<std::chrono::milliseconds> maximumAge) const
436{
437 const GPSSourceHealth* const health = _sourceHealth(kind);
438 auto observation = health ? health->acceptedObservation(use, maximumAge) : std::nullopt;
439 if (observation && kind == SelectedSource::Receiver && _receiverSession != 0 &&
440 observation->sessionId != _receiverSession) {
441 return std::nullopt;
442 }
443 return observation;
444}
445
446GPSSourceHealth* PositionManager::_sourceHealth(SelectedSource kind) const
447{
448 switch (kind) {
450 return _receiver.data();
452 return _device ? _deviceHealth : nullptr;
454 break;
455 }
456 return nullptr;
457}
458
460{
461 switch (_selectedSource) {
463 return tr("None");
465 return tr("GNSS receiver");
467 return _usingPluginSource ? tr("Plugin positioning") : tr("Internal positioning");
468 }
469 return {};
470}
471
473{
474 switch (_sourceStatus) {
476 return tr("Selected position source is not connected");
478 return tr("Waiting for location permission");
480 return tr("Location permission denied");
482 return tr("No internal positioning backend is available");
484 return tr("Waiting for a position fix");
486 return tr("Position is usable");
488 return tr("Position data is stale");
490 return tr("Position fix does not meet accuracy requirements");
491 }
492 return {};
493}
GPSAltitudeDatum
std::shared_ptr< const void > GPSPositionSourceRegistration
Error error
#define QGC_LOGGING_CATEGORY(name, categoryStr)
Session health is independent of transport readiness and RTK survey-in validity.
void positionChanged()
void updateObservation(const GPSObservation &observation)
std::optional< GPSObservation > acceptedObservation(GPSObservation::PositionUse use=GPSObservation::PositionUse::GroundStation, std::optional< std::chrono::milliseconds > maximumAge=std::nullopt) const
void setInternalPositionSource(QGeoPositionInfoSource *source, SourceStatus status, bool custom=false, GPSAltitudeDatum altitudeDatum=GPSAltitudeDatum::Unknown)
QString sourceStatusText() const
~PositionManager() override
void gcsPositionChanged(QGeoCoordinate gcsPosition)
void gcsPositionHorizontalAccuracyChanged(qreal gcsPositionHorizontalAccuracy)
void shutdown()
Releases the position sources and ignores later configuration; init() does not restart them.
std::optional< GPSObservation > acceptedObservation(GPSObservation::PositionUse use=GPSObservation::PositionUse::GroundStation, std::optional< std::chrono::milliseconds > maximumAge=std::nullopt) const
void selectionChanged()
void setConfiguration(const Configuration &configuration)
Ignored after shutdown().
PositionManager(QObject *parent=nullptr, RuntimeScheduler *scheduler=nullptr)
static constexpr std::chrono::milliseconds RECOVERY_DELAY
SourceMode
Values of RTKSettings::gcsPositionSource.
void gcsHeadingChanged(qreal gcsHeading)
GPSPositionSourceRegistration registerReceiver(GPSSourceHealth *producer, quint64 sessionId=0)
std::function< QGeoPositionInfoSource *(QObject *parent)> SourceFactory
Creates a position source parented to parent, or returns null.
const Configuration & configuration() const
void setPlatformSourceFactory(SourceFactory factory)
QString selectedSourceName() const
virtual quint64 nowUs() const =0
qint64 nowMs() const
bool schedule(std::chrono::microseconds delay, RuntimeScheduler::Callback callback)
GPSPositionReport position(const GPSDecodedPosition &source, const GPSIntegrityReport &diagnostic, uint64_t nowUs)
constexpr std::chrono::microseconds remaining(uint64_t timestampUs, uint64_t nowUs, std::chrono::microseconds lifetime)
Time left before a receipt reaches lifetime; zero for a missing, future or expired one.
Receiver-independent data. Unknown metadata remains absent, never a manufactured zero.
QGeoPositionInfo position
quint64 monotonicTimestampUs
GPSAltitudeDatum altitudeDatum