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