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 if (message.msgid == MAVLINK_MSG_ID_RADIO_STATUS) {
130 }
131
132 // v1/v2 share per-(sysid,compid) sequence counters; counting v1 makes every v2 appear lost. Skip v1 non-heartbeats.
133 // RADIO_STATUS is exempt: SiK radios always frame it as v1, so it is processed and never triggers the v1 warning.
134 const bool isV1 = (status.flags & MAVLINK_STATUS_FLAG_IN_MAVLINK1);
135 if (isV1 && (message.msgid != MAVLINK_MSG_ID_HEARTBEAT) && (message.msgid != MAVLINK_MSG_ID_RADIO_STATUS)) {
137 continue;
138 }
139
140 if (!isV1) {
142 _updateCounters(mavlinkChannel, message);
143 }
144 if (!linkPtr->linkConfiguration()->isForwarding()) {
145 _forward(message);
146 _forwardSupport(message);
147 }
148 _logData(link, message);
149
150 if (!_updateStatus(link, linkPtr, mavlinkChannel, message)) {
151 break;
152 }
153 }
154}
155
156void MAVLinkProtocol::_updateCounters(uint8_t mavlinkChannel, const mavlink_message_t& message)
157{
158 _totalReceiveCounter[mavlinkChannel]++;
159
160 uint8_t& lastSeq = _lastIndex[mavlinkChannel][message.sysid][message.compid];
161
162 const QPair<uint8_t, uint8_t> key(message.sysid, message.compid);
163 uint8_t expectedSeq;
164 if (!_firstMessageSeen[mavlinkChannel].contains(key)) {
165 _firstMessageSeen[mavlinkChannel].insert(key);
166 expectedSeq = message.seq;
167 } else if (message.seq == lastSeq) {
168 // v1/v2 of the same message share sequence numbers — duplicate seq isn't loss.
169 return;
170 } else {
171 expectedSeq = lastSeq + 1;
172 }
173
174 uint64_t lostMessages;
175 if (message.seq >= expectedSeq) {
176 lostMessages = message.seq - expectedSeq;
177 } else {
178 lostMessages = static_cast<uint64_t>(message.seq) + 256ULL - expectedSeq;
179 }
180 _totalLossCounter[mavlinkChannel] += lostMessages;
181
182 lastSeq = message.seq;
183
184 const uint64_t totalSent = _totalReceiveCounter[mavlinkChannel] + _totalLossCounter[mavlinkChannel];
185 const float currentLossPercent = (static_cast<double>(_totalLossCounter[mavlinkChannel]) / totalSent) * 100.0f;
186 _runningLossPercent[mavlinkChannel] = (currentLossPercent + _runningLossPercent[mavlinkChannel]) * 0.5f;
187}
188
189void MAVLinkProtocol::_forward(const mavlink_message_t& message)
190{
191 if (message.msgid == MAVLINK_MSG_ID_SETUP_SIGNING) {
192 return;
193 }
194
195 if (!SettingsManager::instance()->mavlinkSettings()->forwardMavlink()->rawValue().toBool()) {
196 return;
197 }
198
200 if (!forwardingLink) {
201 return;
202 }
203
204 // Strip signature on forward: foreign key would BAD_SIGNATURE on downstream signing-aware parsers.
205 const QByteArray bytes = MAVLinkSigning::serializeUnsignedCopy(message);
206 (void)forwardingLink->writeBytesThreadSafe(bytes.constData(), bytes.size());
207}
208
209void MAVLinkProtocol::_forwardSupport(const mavlink_message_t& message)
210{
211 if (message.msgid == MAVLINK_MSG_ID_SETUP_SIGNING) {
212 return;
213 }
214
215 if (!LinkManager::instance()->mavlinkSupportForwardingEnabled()) {
216 return;
217 }
218
220 if (!forwardingSupportLink) {
221 return;
222 }
223
224 const QByteArray bytes = MAVLinkSigning::serializeUnsignedCopy(message);
225 (void)forwardingSupportLink->writeBytesThreadSafe(bytes.constData(), bytes.size());
226}
227
228void MAVLinkProtocol::_logData(LinkInterface* link, const mavlink_message_t& message)
229{
230 if (!_logSuspendError && !_logSuspendReplay && _tempLogFile->isOpen()) {
231 // MAVLink spec §Logging: omit SETUP_SIGNING (contains secret key)
232 if (message.msgid != MAVLINK_MSG_ID_SETUP_SIGNING) {
233 // MAVLink spec §Logging: strip signature block from logged packets.
234 const QByteArray msgBytes = MAVLinkSigning::serializeUnsignedCopy(message);
235 const quint64 timestamp = static_cast<quint64>(QDateTime::currentMSecsSinceEpoch() * 1000);
236 QByteArray log_data;
237 log_data.resize(static_cast<qsizetype>(sizeof(timestamp)) + msgBytes.size());
238 qToBigEndian(timestamp, reinterpret_cast<uint8_t*>(log_data.data()));
239 std::memcpy(log_data.data() + sizeof(timestamp), msgBytes.constData(), msgBytes.size());
240 if (_tempLogFile->write(log_data) != log_data.size()) {
241 const QString logErrorMessage =
242 QStringLiteral("MAVLink Logging failed. Could not write to file %1, logging disabled.")
243 .arg(_tempLogFile->fileName());
244 QGC::showAppMessage(logErrorMessage, getName());
245 _stopLogging();
246 _logSuspendError = true;
247 }
248 }
249
250 if ((message.msgid == MAVLINK_MSG_ID_HEARTBEAT) && !_vehicleWasArmed) {
251 if (mavlink_msg_heartbeat_get_base_mode(&message) & MAV_MODE_FLAG_DECODE_POSITION_SAFETY) {
252 _vehicleWasArmed = true;
253 }
254 }
255 }
256
257 switch (message.msgid) {
258 case MAVLINK_MSG_ID_HEARTBEAT: {
259 _startLogging();
260 mavlink_heartbeat_t heartbeat{};
261 mavlink_msg_heartbeat_decode(&message, &heartbeat);
262 emit vehicleHeartbeatInfo(link, message.sysid, message.compid, heartbeat.autopilot, heartbeat.type);
263 break;
264 }
265 case MAVLINK_MSG_ID_HIGH_LATENCY: {
266 _startLogging();
267 mavlink_high_latency_t highLatency{};
268 mavlink_msg_high_latency_decode(&message, &highLatency);
269 // HIGH_LATENCY does not provide autopilot or type information, generic is our safest bet
270 emit vehicleHeartbeatInfo(link, message.sysid, message.compid, MAV_AUTOPILOT_GENERIC, MAV_TYPE_GENERIC);
271 break;
272 }
273 case MAVLINK_MSG_ID_HIGH_LATENCY2: {
274 _startLogging();
275 mavlink_high_latency2_t highLatency2{};
276 mavlink_msg_high_latency2_decode(&message, &highLatency2);
277 emit vehicleHeartbeatInfo(link, message.sysid, message.compid, highLatency2.autopilot, highLatency2.type);
278 break;
279 }
280 default:
281 break;
282 }
283}
284
285bool MAVLinkProtocol::_updateStatus(LinkInterface* link, const SharedLinkInterfacePtr linkPtr, uint8_t mavlinkChannel,
286 const mavlink_message_t& message)
287{
288 if ((_totalReceiveCounter[mavlinkChannel] % 31) == 0) {
289 const uint64_t totalSent = _totalReceiveCounter[mavlinkChannel] + _totalLossCounter[mavlinkChannel];
290 emit mavlinkMessageStatus(message.sysid, totalSent, _totalReceiveCounter[mavlinkChannel],
291 _totalLossCounter[mavlinkChannel], _runningLossPercent[mavlinkChannel]);
292 }
293
294 emit messageReceived(link, message);
295
296 if (linkPtr.use_count() == 1) {
297 return false;
298 }
299
300 return true;
301}
302
303bool MAVLinkProtocol::_closeLogFile()
304{
305 if (!_tempLogFile->isOpen()) {
306 return false;
307 }
308
309 if (_tempLogFile->size() == 0) {
310 (void)_tempLogFile->remove();
311 return false;
312 }
313
314 (void)_tempLogFile->flush();
315 _tempLogFile->close();
316 return true;
317}
318
319void MAVLinkProtocol::_startLogging()
320{
321 if (QGC::runningUnitTests()) {
322 return;
323 }
324
325 AppSettings* const appSettings = SettingsManager::instance()->appSettings();
326 if (appSettings->disableAllPersistence()->rawValue().toBool()) {
327 return;
328 }
329
330#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
331 if (!SettingsManager::instance()->mavlinkSettings()->telemetrySave()->rawValue().toBool()) {
332 return;
333 }
334#endif
335
336 if (_tempLogFile->isOpen()) {
337 return;
338 }
339
340 if (_logSuspendReplay) {
341 return;
342 }
343
344 // Generate unique temp file path for this logging session
345 const QString logPath =
346 QGCFileHelper::uniqueTempPath(QStringLiteral("%1.%2").arg(_tempLogFileTemplate, _logFileExtension));
347 if (logPath.isEmpty()) {
348 qCWarning(MAVLinkProtocolLog) << "Failed to generate temp log path";
349 _logSuspendError = true;
350 return;
351 }
352
353 _tempLogFile->setFileName(logPath);
354 if (!_tempLogFile->open(QIODevice::WriteOnly)) {
355 const QString message = QStringLiteral(
356 "Opening Flight Data file for writing failed. "
357 "Unable to write to %1. Please choose a different file location.")
358 .arg(_tempLogFile->fileName());
359 QGC::showAppMessage(message, getName());
360 _closeLogFile();
361 _logSuspendError = true;
362 return;
363 }
364
365 qCDebug(MAVLinkProtocolLog) << "Temp log" << _tempLogFile->fileName();
366 (void)_checkTelemetrySavePath();
367
368 _logSuspendError = false;
369}
370
371void MAVLinkProtocol::_stopLogging()
372{
373 if (_tempLogFile->isOpen() && _closeLogFile()) {
374 auto appSettings = SettingsManager::instance()->appSettings();
375 auto mavlinkSettings = SettingsManager::instance()->mavlinkSettings();
376 if ((_vehicleWasArmed || mavlinkSettings->telemetrySaveNotArmed()->rawValue().toBool()) &&
377 mavlinkSettings->telemetrySave()->rawValue().toBool() &&
378 !appSettings->disableAllPersistence()->rawValue().toBool()) {
379 _saveTelemetryLog(_tempLogFile->fileName());
380 } else {
381 (void)QFile::remove(_tempLogFile->fileName());
382 }
383 }
384
385 _vehicleWasArmed = false;
386}
387
389{
390 static const QDir tempDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation));
391 static const QString filter(QStringLiteral("*.%1").arg(_logFileExtension));
392 static const QStringList filterList(filter);
393
394 const QFileInfoList fileInfoList = tempDir.entryInfoList(filterList, QDir::Files);
395 qCDebug(MAVLinkProtocolLog) << "Orphaned log file count" << fileInfoList.count();
396
397 for (const QFileInfo& fileInfo : fileInfoList) {
398 qCDebug(MAVLinkProtocolLog) << "Orphaned log file" << fileInfo.filePath();
399 if (fileInfo.size() == 0) {
400 (void)QFile::remove(fileInfo.filePath());
401 continue;
402 }
403 _saveTelemetryLog(fileInfo.filePath());
404 }
405}
406
408{
409 static const QDir tempDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation));
410 static const QString filter(QStringLiteral("*.%1").arg(_logFileExtension));
411
412 const QFileInfoList fileInfoList = tempDir.entryInfoList(QStringList(filter), QDir::Files);
413 qCDebug(MAVLinkProtocolLog) << "Temp log file count" << fileInfoList.count();
414
415 for (const QFileInfo& fileInfo : fileInfoList) {
416 qCDebug(MAVLinkProtocolLog) << "Temp log file" << fileInfo.filePath();
417 (void)QFile::remove(fileInfo.filePath());
418 }
419}
420
421void MAVLinkProtocol::_saveTelemetryLog(const QString& tempLogfile)
422{
423 if (_checkTelemetrySavePath()) {
424 const QString saveDirPath = SettingsManager::instance()->appSettings()->telemetrySavePath();
425 const QDir saveDir(saveDirPath);
426
427 const QString nameFormat("%1%2.%3");
428 const QString dtFormat("yyyy-MM-dd hh-mm-ss");
429
430 int tryIndex = 1;
431 QString saveFileName = nameFormat.arg(QDateTime::currentDateTime().toString(dtFormat), QString(),
433 while (saveDir.exists(saveFileName)) {
434 saveFileName = nameFormat.arg(QDateTime::currentDateTime().toString(dtFormat),
435 QStringLiteral(".%1").arg(tryIndex++), AppSettings::telemetryFileExtension);
436 }
437
438 const QString saveFilePath = saveDir.absoluteFilePath(saveFileName);
439
440 QFile in(tempLogfile);
441 if (!in.open(QIODevice::ReadOnly)) {
442 const QString error =
443 tr("Unable to save telemetry log. Error opening source '%1': '%2'.").arg(tempLogfile, in.errorString());
445 (void)QFile::remove(tempLogfile);
446 return;
447 }
448
449 QSaveFile out(saveFilePath);
450 out.setDirectWriteFallback(true); // allows non-atomic fallback where rename isn’t possible
451
452 if (!out.open(QIODevice::WriteOnly)) {
453 const QString error = tr("Unable to save telemetry log. Error opening destination '%1': '%2'.")
454 .arg(saveFilePath, out.errorString());
456 (void)QFile::remove(tempLogfile);
457 return;
458 }
459
460 // Stream copy to avoid large allocations.
461 QByteArray buffer;
462 constexpr int bufferSize = 256 * 1024; // 256 KiB
463 buffer.resize(bufferSize);
464 while (true) {
465 const qint64 n = in.read(buffer.data(), buffer.size());
466 if (n == 0) {
467 break;
468 }
469 if (n < 0) {
470 const QString error = tr("Unable to save telemetry log. Error reading source '%1': '%2'.")
471 .arg(tempLogfile, in.errorString());
473 out.cancelWriting();
474 (void)QFile::remove(tempLogfile);
475 return;
476 }
477 if (out.write(buffer.constData(), n) != n) {
478 const QString error = tr("Unable to save telemetry log. Error writing destination '%1': '%2'.")
479 .arg(saveFilePath, out.errorString());
481 out.cancelWriting();
482 (void)QFile::remove(tempLogfile);
483 return;
484 }
485 }
486
487 if (!out.commit()) {
488 const QString error =
489 tr("Unable to finalize telemetry log '%1': '%2'.").arg(saveFilePath, out.errorString());
491 (void)QFile::remove(tempLogfile);
492 return;
493 }
494
495 constexpr QFileDevice::Permissions perms =
496 QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ReadGroup | QFileDevice::ReadOther;
497 (void)out.setPermissions(perms);
498 }
499
500 (void)QFile::remove(tempLogfile);
501}
502
503bool MAVLinkProtocol::_checkTelemetrySavePath()
504{
505 const QString saveDirPath = SettingsManager::instance()->appSettings()->telemetrySavePath();
506 if (saveDirPath.isEmpty()) {
507 const QString error = tr("Unable to save telemetry log. Application save directory is not set.");
509 return false;
510 }
511
512 const QDir saveDir(saveDirPath);
513 if (!saveDir.exists()) {
514 const QString error =
515 tr("Unable to save telemetry log. Telemetry save directory \"%1\" does not exist.").arg(saveDirPath);
517 return false;
518 }
519
520 return true;
521}
522
523void MAVLinkProtocol::_vehicleCountChanged()
524{
525 if (MultiVehicleManager::instance()->vehicles()->count() == 0) {
526 _stopLogging();
527 }
528}
529
531{
532 return SettingsManager::instance()->mavlinkSettings()->gcsMavlinkSystemID()->rawValue().toInt();
533}
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()
void reportMavlinkV2Traffic()
Called when a v2 message is received: permanently suppresses the v1-only warning for this link.
SigningController * signing()
Per-link signing state and confirmation state machine. Non-null after channel allocation.
void reportRadioStatusReceived()
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