QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
MAVLinkProtocol.cc
Go to the documentation of this file.
1#include "MAVLinkProtocol.h"
2
3#include <QtCore/QApplicationStatic>
4#include <QtCore/QDir>
5#include <QtCore/QFile>
6#include <QtCore/QFileInfo>
7#include <QtCore/QMetaType>
8#include <QtCore/QSettings>
9#include <QtCore/QStandardPaths>
10#include <QtCore/QTimer>
11#include <cstring>
12
13#include "AppMessages.h"
14#include "AppSettings.h"
15#include "LinkManager.h"
16#include "MAVLinkLib.h"
17#include "LinkInterface.h"
18#include "MAVLinkSigning.h"
19#include "SigningController.h"
20#include "MavlinkSettings.h"
21#include "MultiVehicleManager.h"
22#include "QGCFileHelper.h"
23#include "QGCLoggingCategory.h"
24#include "QmlObjectListModel.h"
25#include "SettingsManager.h"
26
27QGC_LOGGING_CATEGORY(MAVLinkProtocolLog, "Comms.MAVLinkProtocol")
28
29Q_APPLICATION_STATIC(MAVLinkProtocol, _mavlinkProtocolInstance);
30
31MAVLinkProtocol::MAVLinkProtocol(QObject* parent) : QObject(parent), _tempLogFile(new QFile(this))
32{
33 qCDebug(MAVLinkProtocolLog) << this;
34}
35
37{
38 _closeLogFile();
39
40 qCDebug(MAVLinkProtocolLog) << this;
41}
42
44{
45 return _mavlinkProtocolInstance();
46}
47
49{
50 if (_initialized) {
51 return;
52 }
53
55 &MAVLinkProtocol::_vehicleCountChanged);
56
57 _initialized = true;
58}
59
61{
62 const uint8_t channel = link->mavlinkChannel();
63 _totalReceiveCounter[channel] = 0;
64 _totalLossCounter[channel] = 0;
65 _runningLossPercent[channel] = 0.f;
66
68}
69
71{
72 // Clear per-(sysid,compid) sequence state so next packet isn't counted as a gap.
73 const uint8_t channel = link->mavlinkChannel();
74 _firstMessageSeen[channel].clear();
75 std::memset(_lastIndex[channel], 0, sizeof(_lastIndex[channel]));
76}
77
78void MAVLinkProtocol::logSentBytes(const LinkInterface* link, const QByteArray& data)
79{
80 Q_UNUSED(link);
81
82 if (_logSuspendError || _logSuspendReplay || !_tempLogFile->isOpen()) {
83 return;
84 }
85
86 const quint64 time = static_cast<quint64>(QDateTime::currentMSecsSinceEpoch() * 1000);
87 uint8_t bytes_time[sizeof(quint64)]{};
88 qToBigEndian(time, bytes_time);
89
90 QByteArray logData = data;
91 QByteArray timeData = QByteArray::fromRawData(reinterpret_cast<const char*>(bytes_time), sizeof(bytes_time));
92 (void)logData.prepend(timeData);
93 if (_tempLogFile->write(logData) != logData.length()) {
94 const QString message = QStringLiteral("MAVLink Logging failed. Could not write to file %1, logging disabled.")
95 .arg(_tempLogFile->fileName());
96 QGC::showAppMessage(message, getName());
97 _stopLogging();
98 _logSuspendError = true;
99 }
100}
101
102void MAVLinkProtocol::receiveBytes(LinkInterface* link, const QByteArray& data)
103{
105 if (!linkPtr) {
106 qCDebug(MAVLinkProtocolLog) << "receiveBytes: link gone!" << data.size() << "bytes arrived too late";
107 return;
108 }
109
110 for (uint8_t byte : data) {
111 const uint8_t mavlinkChannel = link->mavlinkChannel();
112 mavlink_message_t message{};
113 mavlink_status_t status{};
114
115 const uint8_t framing = mavlink_parse_char(mavlinkChannel, byte, &message, &status);
116 if (framing == MAVLINK_FRAMING_OK || framing == MAVLINK_FRAMING_BAD_SIGNATURE) {
117 if (SigningController* const sigCtrl = link->signing()) {
118 // Auto-detected key: reset sequence tracking so the key-install gap isn't counted as loss.
119 if (sigCtrl->processFrame(framing == MAVLINK_FRAMING_OK, message)) {
121 }
122 }
123 }
124 if (framing != MAVLINK_FRAMING_OK) {
125 continue;
126 }
127
128 // v1/v2 share per-(sysid,compid) sequence counters; counting v1 makes every v2 appear lost. Skip v1 non-heartbeats.
129 const bool isV1 = (status.flags & MAVLINK_STATUS_FLAG_IN_MAVLINK1);
130 if (isV1 && message.msgid != MAVLINK_MSG_ID_HEARTBEAT) {
132 continue;
133 }
134
135 if (!isV1) {
136 _updateCounters(mavlinkChannel, message);
137 }
138 if (!linkPtr->linkConfiguration()->isForwarding()) {
139 _forward(message);
140 _forwardSupport(message);
141 }
142 _logData(link, message);
143
144 if (!_updateStatus(link, linkPtr, mavlinkChannel, message)) {
145 break;
146 }
147 }
148}
149
150void MAVLinkProtocol::_updateCounters(uint8_t mavlinkChannel, const mavlink_message_t& message)
151{
152 _totalReceiveCounter[mavlinkChannel]++;
153
154 uint8_t& lastSeq = _lastIndex[mavlinkChannel][message.sysid][message.compid];
155
156 const QPair<uint8_t, uint8_t> key(message.sysid, message.compid);
157 uint8_t expectedSeq;
158 if (!_firstMessageSeen[mavlinkChannel].contains(key)) {
159 _firstMessageSeen[mavlinkChannel].insert(key);
160 expectedSeq = message.seq;
161 } else if (message.seq == lastSeq) {
162 // v1/v2 of the same message share sequence numbers — duplicate seq isn't loss.
163 return;
164 } else {
165 expectedSeq = lastSeq + 1;
166 }
167
168 uint64_t lostMessages;
169 if (message.seq >= expectedSeq) {
170 lostMessages = message.seq - expectedSeq;
171 } else {
172 lostMessages = static_cast<uint64_t>(message.seq) + 256ULL - expectedSeq;
173 }
174 _totalLossCounter[mavlinkChannel] += lostMessages;
175
176 lastSeq = message.seq;
177
178 const uint64_t totalSent = _totalReceiveCounter[mavlinkChannel] + _totalLossCounter[mavlinkChannel];
179 const float currentLossPercent = (static_cast<double>(_totalLossCounter[mavlinkChannel]) / totalSent) * 100.0f;
180 _runningLossPercent[mavlinkChannel] = (currentLossPercent + _runningLossPercent[mavlinkChannel]) * 0.5f;
181}
182
183void MAVLinkProtocol::_forward(const mavlink_message_t& message)
184{
185 if (message.msgid == MAVLINK_MSG_ID_SETUP_SIGNING) {
186 return;
187 }
188
189 if (!SettingsManager::instance()->mavlinkSettings()->forwardMavlink()->rawValue().toBool()) {
190 return;
191 }
192
194 if (!forwardingLink) {
195 return;
196 }
197
198 // Strip signature on forward: foreign key would BAD_SIGNATURE on downstream signing-aware parsers.
199 const QByteArray bytes = MAVLinkSigning::serializeUnsignedCopy(message);
200 (void)forwardingLink->writeBytesThreadSafe(bytes.constData(), bytes.size());
201}
202
203void MAVLinkProtocol::_forwardSupport(const mavlink_message_t& message)
204{
205 if (message.msgid == MAVLINK_MSG_ID_SETUP_SIGNING) {
206 return;
207 }
208
209 if (!LinkManager::instance()->mavlinkSupportForwardingEnabled()) {
210 return;
211 }
212
214 if (!forwardingSupportLink) {
215 return;
216 }
217
218 const QByteArray bytes = MAVLinkSigning::serializeUnsignedCopy(message);
219 (void)forwardingSupportLink->writeBytesThreadSafe(bytes.constData(), bytes.size());
220}
221
222void MAVLinkProtocol::_logData(LinkInterface* link, const mavlink_message_t& message)
223{
224 if (!_logSuspendError && !_logSuspendReplay && _tempLogFile->isOpen()) {
225 // MAVLink spec §Logging: omit SETUP_SIGNING (contains secret key)
226 if (message.msgid != MAVLINK_MSG_ID_SETUP_SIGNING) {
227 // MAVLink spec §Logging: strip signature block from logged packets.
228 const QByteArray msgBytes = MAVLinkSigning::serializeUnsignedCopy(message);
229 const quint64 timestamp = static_cast<quint64>(QDateTime::currentMSecsSinceEpoch() * 1000);
230 QByteArray log_data;
231 log_data.resize(static_cast<qsizetype>(sizeof(timestamp)) + msgBytes.size());
232 qToBigEndian(timestamp, reinterpret_cast<uint8_t*>(log_data.data()));
233 std::memcpy(log_data.data() + sizeof(timestamp), msgBytes.constData(), msgBytes.size());
234 if (_tempLogFile->write(log_data) != log_data.size()) {
235 const QString logErrorMessage =
236 QStringLiteral("MAVLink Logging failed. Could not write to file %1, logging disabled.")
237 .arg(_tempLogFile->fileName());
238 QGC::showAppMessage(logErrorMessage, getName());
239 _stopLogging();
240 _logSuspendError = true;
241 }
242 }
243
244 if ((message.msgid == MAVLINK_MSG_ID_HEARTBEAT) && !_vehicleWasArmed) {
245 if (mavlink_msg_heartbeat_get_base_mode(&message) & MAV_MODE_FLAG_DECODE_POSITION_SAFETY) {
246 _vehicleWasArmed = true;
247 }
248 }
249 }
250
251 switch (message.msgid) {
252 case MAVLINK_MSG_ID_HEARTBEAT: {
253 _startLogging();
254 mavlink_heartbeat_t heartbeat{};
255 mavlink_msg_heartbeat_decode(&message, &heartbeat);
256 emit vehicleHeartbeatInfo(link, message.sysid, message.compid, heartbeat.autopilot, heartbeat.type);
257 break;
258 }
259 case MAVLINK_MSG_ID_HIGH_LATENCY: {
260 _startLogging();
261 mavlink_high_latency_t highLatency{};
262 mavlink_msg_high_latency_decode(&message, &highLatency);
263 // HIGH_LATENCY does not provide autopilot or type information, generic is our safest bet
264 emit vehicleHeartbeatInfo(link, message.sysid, message.compid, MAV_AUTOPILOT_GENERIC, MAV_TYPE_GENERIC);
265 break;
266 }
267 case MAVLINK_MSG_ID_HIGH_LATENCY2: {
268 _startLogging();
269 mavlink_high_latency2_t highLatency2{};
270 mavlink_msg_high_latency2_decode(&message, &highLatency2);
271 emit vehicleHeartbeatInfo(link, message.sysid, message.compid, highLatency2.autopilot, highLatency2.type);
272 break;
273 }
274 default:
275 break;
276 }
277}
278
279bool MAVLinkProtocol::_updateStatus(LinkInterface* link, const SharedLinkInterfacePtr linkPtr, uint8_t mavlinkChannel,
280 const mavlink_message_t& message)
281{
282 if ((_totalReceiveCounter[mavlinkChannel] % 31) == 0) {
283 const uint64_t totalSent = _totalReceiveCounter[mavlinkChannel] + _totalLossCounter[mavlinkChannel];
284 emit mavlinkMessageStatus(message.sysid, totalSent, _totalReceiveCounter[mavlinkChannel],
285 _totalLossCounter[mavlinkChannel], _runningLossPercent[mavlinkChannel]);
286 }
287
288 emit messageReceived(link, message);
289
290 if (linkPtr.use_count() == 1) {
291 return false;
292 }
293
294 return true;
295}
296
297bool MAVLinkProtocol::_closeLogFile()
298{
299 if (!_tempLogFile->isOpen()) {
300 return false;
301 }
302
303 if (_tempLogFile->size() == 0) {
304 (void)_tempLogFile->remove();
305 return false;
306 }
307
308 (void)_tempLogFile->flush();
309 _tempLogFile->close();
310 return true;
311}
312
313void MAVLinkProtocol::_startLogging()
314{
315 if (QGC::runningUnitTests()) {
316 return;
317 }
318
319 AppSettings* const appSettings = SettingsManager::instance()->appSettings();
320 if (appSettings->disableAllPersistence()->rawValue().toBool()) {
321 return;
322 }
323
324#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
325 if (!SettingsManager::instance()->mavlinkSettings()->telemetrySave()->rawValue().toBool()) {
326 return;
327 }
328#endif
329
330 if (_tempLogFile->isOpen()) {
331 return;
332 }
333
334 if (_logSuspendReplay) {
335 return;
336 }
337
338 // Generate unique temp file path for this logging session
339 const QString logPath =
340 QGCFileHelper::uniqueTempPath(QStringLiteral("%1.%2").arg(_tempLogFileTemplate, _logFileExtension));
341 if (logPath.isEmpty()) {
342 qCWarning(MAVLinkProtocolLog) << "Failed to generate temp log path";
343 _logSuspendError = true;
344 return;
345 }
346
347 _tempLogFile->setFileName(logPath);
348 if (!_tempLogFile->open(QIODevice::WriteOnly)) {
349 const QString message = QStringLiteral(
350 "Opening Flight Data file for writing failed. "
351 "Unable to write to %1. Please choose a different file location.")
352 .arg(_tempLogFile->fileName());
353 QGC::showAppMessage(message, getName());
354 _closeLogFile();
355 _logSuspendError = true;
356 return;
357 }
358
359 qCDebug(MAVLinkProtocolLog) << "Temp log" << _tempLogFile->fileName();
360 (void)_checkTelemetrySavePath();
361
362 _logSuspendError = false;
363}
364
365void MAVLinkProtocol::_stopLogging()
366{
367 if (_tempLogFile->isOpen() && _closeLogFile()) {
368 auto appSettings = SettingsManager::instance()->appSettings();
369 auto mavlinkSettings = SettingsManager::instance()->mavlinkSettings();
370 if ((_vehicleWasArmed || mavlinkSettings->telemetrySaveNotArmed()->rawValue().toBool()) &&
371 mavlinkSettings->telemetrySave()->rawValue().toBool() &&
372 !appSettings->disableAllPersistence()->rawValue().toBool()) {
373 _saveTelemetryLog(_tempLogFile->fileName());
374 } else {
375 (void)QFile::remove(_tempLogFile->fileName());
376 }
377 }
378
379 _vehicleWasArmed = false;
380}
381
383{
384 static const QDir tempDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation));
385 static const QString filter(QStringLiteral("*.%1").arg(_logFileExtension));
386 static const QStringList filterList(filter);
387
388 const QFileInfoList fileInfoList = tempDir.entryInfoList(filterList, QDir::Files);
389 qCDebug(MAVLinkProtocolLog) << "Orphaned log file count" << fileInfoList.count();
390
391 for (const QFileInfo& fileInfo : fileInfoList) {
392 qCDebug(MAVLinkProtocolLog) << "Orphaned log file" << fileInfo.filePath();
393 if (fileInfo.size() == 0) {
394 (void)QFile::remove(fileInfo.filePath());
395 continue;
396 }
397 _saveTelemetryLog(fileInfo.filePath());
398 }
399}
400
402{
403 static const QDir tempDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation));
404 static const QString filter(QStringLiteral("*.%1").arg(_logFileExtension));
405
406 const QFileInfoList fileInfoList = tempDir.entryInfoList(QStringList(filter), QDir::Files);
407 qCDebug(MAVLinkProtocolLog) << "Temp log file count" << fileInfoList.count();
408
409 for (const QFileInfo& fileInfo : fileInfoList) {
410 qCDebug(MAVLinkProtocolLog) << "Temp log file" << fileInfo.filePath();
411 (void)QFile::remove(fileInfo.filePath());
412 }
413}
414
415void MAVLinkProtocol::_saveTelemetryLog(const QString& tempLogfile)
416{
417 if (_checkTelemetrySavePath()) {
418 const QString saveDirPath = SettingsManager::instance()->appSettings()->telemetrySavePath();
419 const QDir saveDir(saveDirPath);
420
421 const QString nameFormat("%1%2.%3");
422 const QString dtFormat("yyyy-MM-dd hh-mm-ss");
423
424 int tryIndex = 1;
425 QString saveFileName = nameFormat.arg(QDateTime::currentDateTime().toString(dtFormat), QString(),
427 while (saveDir.exists(saveFileName)) {
428 saveFileName = nameFormat.arg(QDateTime::currentDateTime().toString(dtFormat),
429 QStringLiteral(".%1").arg(tryIndex++), AppSettings::telemetryFileExtension);
430 }
431
432 const QString saveFilePath = saveDir.absoluteFilePath(saveFileName);
433
434 QFile in(tempLogfile);
435 if (!in.open(QIODevice::ReadOnly)) {
436 const QString error =
437 tr("Unable to save telemetry log. Error opening source '%1': '%2'.").arg(tempLogfile, in.errorString());
439 (void)QFile::remove(tempLogfile);
440 return;
441 }
442
443 QSaveFile out(saveFilePath);
444 out.setDirectWriteFallback(true); // allows non-atomic fallback where rename isn’t possible
445
446 if (!out.open(QIODevice::WriteOnly)) {
447 const QString error = tr("Unable to save telemetry log. Error opening destination '%1': '%2'.")
448 .arg(saveFilePath, out.errorString());
450 (void)QFile::remove(tempLogfile);
451 return;
452 }
453
454 // Stream copy to avoid large allocations.
455 QByteArray buffer;
456 constexpr int bufferSize = 256 * 1024; // 256 KiB
457 buffer.resize(bufferSize);
458 while (true) {
459 const qint64 n = in.read(buffer.data(), buffer.size());
460 if (n == 0) {
461 break;
462 }
463 if (n < 0) {
464 const QString error = tr("Unable to save telemetry log. Error reading source '%1': '%2'.")
465 .arg(tempLogfile, in.errorString());
467 out.cancelWriting();
468 (void)QFile::remove(tempLogfile);
469 return;
470 }
471 if (out.write(buffer.constData(), n) != n) {
472 const QString error = tr("Unable to save telemetry log. Error writing destination '%1': '%2'.")
473 .arg(saveFilePath, out.errorString());
475 out.cancelWriting();
476 (void)QFile::remove(tempLogfile);
477 return;
478 }
479 }
480
481 if (!out.commit()) {
482 const QString error =
483 tr("Unable to finalize telemetry log '%1': '%2'.").arg(saveFilePath, out.errorString());
485 (void)QFile::remove(tempLogfile);
486 return;
487 }
488
489 constexpr QFileDevice::Permissions perms =
490 QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ReadGroup | QFileDevice::ReadOther;
491 (void)out.setPermissions(perms);
492 }
493
494 (void)QFile::remove(tempLogfile);
495}
496
497bool MAVLinkProtocol::_checkTelemetrySavePath()
498{
499 const QString saveDirPath = SettingsManager::instance()->appSettings()->telemetrySavePath();
500 if (saveDirPath.isEmpty()) {
501 const QString error = tr("Unable to save telemetry log. Application save directory is not set.");
503 return false;
504 }
505
506 const QDir saveDir(saveDirPath);
507 if (!saveDir.exists()) {
508 const QString error =
509 tr("Unable to save telemetry log. Telemetry save directory \"%1\" does not exist.").arg(saveDirPath);
511 return false;
512 }
513
514 return true;
515}
516
517void MAVLinkProtocol::_vehicleCountChanged()
518{
519 if (MultiVehicleManager::instance()->vehicles()->count() == 0) {
520 _stopLogging();
521 }
522}
523
525{
526 return SettingsManager::instance()->mavlinkSettings()->gcsMavlinkSystemID()->rawValue().toInt();
527}
std::shared_ptr< LinkInterface > SharedLinkInterfacePtr
Q_APPLICATION_STATIC(MAVLinkProtocol, _mavlinkProtocolInstance)
Error error
struct __mavlink_message mavlink_message_t
#define QGC_LOGGING_CATEGORY(name, categoryStr)
struct __mavlink_high_latency2_t mavlink_high_latency2_t
Application Settings.
Definition AppSettings.h:10
QString telemetrySavePath()
static constexpr const char * telemetryFileExtension
The link interface defines the interface for all links used to communicate with the ground station ap...
void setDecodedFirstMavlinkPacket(bool decodedFirstMavlinkPacket)
uint8_t mavlinkChannel() const
void reportMavlinkV1Traffic()
SigningController * signing()
Per-link signing state and confirmation state machine. Non-null after channel allocation.
SharedLinkInterfacePtr sharedLinkInterfacePointerForLink(const LinkInterface *link)
static LinkManager * instance()
SharedLinkInterfacePtr mavlinkForwardingSupportLink()
Returns pointer to the mavlink support forwarding link, or nullptr if it does not exist.
SharedLinkInterfacePtr mavlinkForwardingLink()
Returns pointer to the mavlink forwarding link, or nullptr if it does not exist.
MAVLink micro air vehicle protocol reference implementation.
void receiveBytes(LinkInterface *link, const QByteArray &data)
void mavlinkMessageStatus(int sysid, uint64_t totalSent, uint64_t totalReceived, uint64_t totalLoss, float lossPercent)
void messageReceived(LinkInterface *link, const mavlink_message_t &message)
void vehicleHeartbeatInfo(LinkInterface *link, int vehicleId, int componentId, int vehicleFirmwareType, int vehicleType)
void resetSequenceTracking(LinkInterface *link)
Reset sequence tracking so signing transitions don't inflate loss counters.
void logSentBytes(const LinkInterface *link, const QByteArray &data)
static MAVLinkProtocol * instance()
int getSystemId() const
void resetMetadataForLink(LinkInterface *link)
static QString getName()
static void deleteTempLogFiles()
static MultiVehicleManager * instance()
void vehicleRemoved(Vehicle *vehicle)
static SettingsManager * instance()
AppSettings * appSettings() const
MavlinkSettings * mavlinkSettings() const
Owns MAVLink signing state and the deferred-confirmation state machine for one LinkInterface.
QByteArray serializeUnsignedCopy(const mavlink_message_t &message)
QString uniqueTempPath(const QString &templateName)
bool runningUnitTests()
void showAppMessage(const QString &message, const QString &title)
Modal application message. Queued if the UI isn't ready yet.
Definition AppMessages.cc:9