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#include "AppSettings.h"
3#include "LinkManager.h"
4#include "MavlinkSettings.h"
6#include "QGCApplication.h"
7#include "QGCFileHelper.h"
10#include "SettingsManager.h"
11
12#include <QtCore/QApplicationStatic>
13#include <QtCore/QDir>
14#include <QtCore/QFile>
15#include <QtCore/QFileInfo>
16#include <QtCore/QMetaType>
17#include <QtCore/QSettings>
18#include <QtCore/QStandardPaths>
19#include <QtCore/QTimer>
20
21QGC_LOGGING_CATEGORY(MAVLinkProtocolLog, "Comms.MAVLinkProtocol")
22
23Q_APPLICATION_STATIC(MAVLinkProtocol, _mavlinkProtocolInstance);
24
26 : QObject(parent)
27 , _tempLogFile(new QFile(this))
28{
29 qCDebug(MAVLinkProtocolLog) << this;
30}
31
33{
34 _closeLogFile();
35
36 qCDebug(MAVLinkProtocolLog) << this;
37}
38
40{
41 return _mavlinkProtocolInstance();
42}
43
45{
46 if (_initialized) {
47 return;
48 }
49
50 (void) connect(MultiVehicleManager::instance(), &MultiVehicleManager::vehicleRemoved, this, &MAVLinkProtocol::_vehicleCountChanged);
51
52 _initialized = true;
53}
54
56{
57 const uint8_t channel = link->mavlinkChannel();
58 _totalReceiveCounter[channel] = 0;
59 _totalLossCounter[channel] = 0;
60 _runningLossPercent[channel] = 0.f;
61
63}
64
65void MAVLinkProtocol::logSentBytes(const LinkInterface *link, const QByteArray &data)
66{
67 Q_UNUSED(link);
68
69 if (_logSuspendError || _logSuspendReplay || !_tempLogFile->isOpen()) {
70 return;
71 }
72
73 const quint64 time = static_cast<quint64>(QDateTime::currentMSecsSinceEpoch() * 1000);
74 uint8_t bytes_time[sizeof(quint64)]{};
75 qToBigEndian(time, bytes_time);
76
77 QByteArray logData = data;
78 QByteArray timeData = QByteArray::fromRawData(reinterpret_cast<const char*>(bytes_time), sizeof(bytes_time));
79 (void) logData.prepend(timeData);
80 if (_tempLogFile->write(logData) != logData.length()) {
81 const QString message = QStringLiteral("MAVLink Logging failed. Could not write to file %1, logging disabled.").arg(_tempLogFile->fileName());
82 qgcApp()->showAppMessage(message, getName());
83 _stopLogging();
84 _logSuspendError = true;
85 }
86}
87
88void MAVLinkProtocol::receiveBytes(LinkInterface *link, const QByteArray &data)
89{
90 const SharedLinkInterfacePtr linkPtr = LinkManager::instance()->sharedLinkInterfacePointerForLink(link);
91 if (!linkPtr) {
92 qCDebug(MAVLinkProtocolLog) << "receiveBytes: link gone!" << data.size() << "bytes arrived too late";
93 return;
94 }
95
96 for (uint8_t byte: data) {
97 const uint8_t mavlinkChannel = link->mavlinkChannel();
98 mavlink_message_t message{};
99 mavlink_status_t status{};
100
101 if (mavlink_parse_char(mavlinkChannel, byte, &message, &status) != MAVLINK_FRAMING_OK) {
102 continue;
103 }
104
105 // It's ok to get v1 HEARTBEAT messages on a v2 link:
106 // PX4 defaults to sending V1 then switches to V2 after receiving a V2 message from GCS
107 // ArduPilot always sends both versions
108 if (message.msgid != MAVLINK_MSG_ID_HEARTBEAT && (status.flags & MAVLINK_STATUS_FLAG_IN_MAVLINK1)) {
110 continue;
111 }
112
113 _updateCounters(mavlinkChannel, message);
114 if (!linkPtr->linkConfiguration()->isForwarding()) {
115 _forward(message);
116 _forwardSupport(message);
117 }
118 _logData(link, message);
119
120 if (!_updateStatus(link, linkPtr, mavlinkChannel, message)) {
121 break;
122 }
123 }
124}
125
126void MAVLinkProtocol::_updateCounters(uint8_t mavlinkChannel, const mavlink_message_t &message)
127{
128 _totalReceiveCounter[mavlinkChannel]++;
129
130 uint8_t &lastSeq = _lastIndex[message.sysid][message.compid];
131
132 const QPair<uint8_t,uint8_t> key(message.sysid, message.compid);
133 uint8_t expectedSeq;
134 if (!_firstMessageSeen.contains(key)) {
135 _firstMessageSeen.insert(key);
136 expectedSeq = message.seq;
137 } else {
138 expectedSeq = lastSeq + 1;
139 }
140
141 uint64_t lostMessages;
142 if (message.seq >= expectedSeq) {
143 lostMessages = message.seq - expectedSeq;
144 } else {
145 lostMessages = static_cast<uint64_t>(message.seq) + 256ULL - expectedSeq;
146 }
147 _totalLossCounter[mavlinkChannel] += lostMessages;
148
149 lastSeq = message.seq;
150
151 const uint64_t totalSent = _totalReceiveCounter[mavlinkChannel] + _totalLossCounter[mavlinkChannel];
152 const float currentLossPercent = (static_cast<double>(_totalLossCounter[mavlinkChannel]) / totalSent) * 100.0f;
153 _runningLossPercent[mavlinkChannel] = (currentLossPercent + _runningLossPercent[mavlinkChannel]) * 0.5f;
154}
155
156void MAVLinkProtocol::_forward(const mavlink_message_t &message)
157{
158 if (message.msgid == MAVLINK_MSG_ID_SETUP_SIGNING) {
159 return;
160 }
161
162 if (!SettingsManager::instance()->mavlinkSettings()->forwardMavlink()->rawValue().toBool()) {
163 return;
164 }
165
166 SharedLinkInterfacePtr forwardingLink = LinkManager::instance()->mavlinkForwardingLink();
167 if (!forwardingLink) {
168 return;
169 }
170
171 uint8_t buf[MAVLINK_MAX_PACKET_LEN]{};
172 const uint16_t len = mavlink_msg_to_send_buffer(buf, &message);
173 (void) forwardingLink->writeBytesThreadSafe(reinterpret_cast<const char*>(buf), len);
174}
175
176void MAVLinkProtocol::_forwardSupport(const mavlink_message_t &message)
177{
178 if (message.msgid == MAVLINK_MSG_ID_SETUP_SIGNING) {
179 return;
180 }
181
182 if (!LinkManager::instance()->mavlinkSupportForwardingEnabled()) {
183 return;
184 }
185
186 SharedLinkInterfacePtr forwardingSupportLink = LinkManager::instance()->mavlinkForwardingSupportLink();
187 if (!forwardingSupportLink) {
188 return;
189 }
190
191 uint8_t buf[MAVLINK_MAX_PACKET_LEN]{};
192 const uint16_t len = mavlink_msg_to_send_buffer(buf, &message);
193 (void) forwardingSupportLink->writeBytesThreadSafe(reinterpret_cast<const char*>(buf), len);
194}
195
196void MAVLinkProtocol::_logData(LinkInterface *link, const mavlink_message_t &message)
197{
198 if (!_logSuspendError && !_logSuspendReplay && _tempLogFile->isOpen()) {
199 const quint64 timestamp = static_cast<quint64>(QDateTime::currentMSecsSinceEpoch() * 1000);
200 uint8_t buf[MAVLINK_MAX_PACKET_LEN + sizeof(timestamp)]{};
201 qToBigEndian(timestamp, buf);
202
203 const qsizetype len = mavlink_msg_to_send_buffer(buf + sizeof(timestamp), &message) + sizeof(timestamp);
204 const QByteArray log_data(reinterpret_cast<const char*>(buf), len);
205 if (_tempLogFile->write(log_data) != len) {
206 const QString logErrorMessage = QStringLiteral("MAVLink Logging failed. Could not write to file %1, logging disabled.").arg(_tempLogFile->fileName());
207 qgcApp()->showAppMessage(logErrorMessage, getName());
208 _stopLogging();
209 _logSuspendError = true;
210 }
211
212 if ((message.msgid == MAVLINK_MSG_ID_HEARTBEAT) && !_vehicleWasArmed) {
213 if (mavlink_msg_heartbeat_get_base_mode(&message) & MAV_MODE_FLAG_DECODE_POSITION_SAFETY) {
214 _vehicleWasArmed = true;
215 }
216 }
217 }
218
219 switch (message.msgid) {
220 case MAVLINK_MSG_ID_HEARTBEAT: {
221 _startLogging();
222 mavlink_heartbeat_t heartbeat{};
223 mavlink_msg_heartbeat_decode(&message, &heartbeat);
224 emit vehicleHeartbeatInfo(link, message.sysid, message.compid, heartbeat.autopilot, heartbeat.type);
225 break;
226 }
227 case MAVLINK_MSG_ID_HIGH_LATENCY: {
228 _startLogging();
229 mavlink_high_latency_t highLatency{};
230 mavlink_msg_high_latency_decode(&message, &highLatency);
231 // HIGH_LATENCY does not provide autopilot or type information, generic is our safest bet
232 emit vehicleHeartbeatInfo(link, message.sysid, message.compid, MAV_AUTOPILOT_GENERIC, MAV_TYPE_GENERIC);
233 break;
234 }
235 case MAVLINK_MSG_ID_HIGH_LATENCY2: {
236 _startLogging();
237 mavlink_high_latency2_t highLatency2{};
238 mavlink_msg_high_latency2_decode(&message, &highLatency2);
239 emit vehicleHeartbeatInfo(link, message.sysid, message.compid, highLatency2.autopilot, highLatency2.type);
240 break;
241 }
242 default:
243 break;
244 }
245}
246
247bool MAVLinkProtocol::_updateStatus(LinkInterface *link, const SharedLinkInterfacePtr linkPtr, uint8_t mavlinkChannel, const mavlink_message_t &message)
248{
249 if ((_totalReceiveCounter[mavlinkChannel] % 31) == 0) {
250 const uint64_t totalSent = _totalReceiveCounter[mavlinkChannel] + _totalLossCounter[mavlinkChannel];
251 emit mavlinkMessageStatus(message.sysid, totalSent, _totalReceiveCounter[mavlinkChannel], _totalLossCounter[mavlinkChannel], _runningLossPercent[mavlinkChannel]);
252 }
253
254 emit messageReceived(link, message);
255
256 if (linkPtr.use_count() == 1) {
257 return false;
258 }
259
260 return true;
261}
262
263bool MAVLinkProtocol::_closeLogFile()
264{
265 if (!_tempLogFile->isOpen()) {
266 return false;
267 }
268
269 if (_tempLogFile->size() == 0) {
270 (void) _tempLogFile->remove();
271 return false;
272 }
273
274 (void) _tempLogFile->flush();
275 _tempLogFile->close();
276 return true;
277}
278
279void MAVLinkProtocol::_startLogging()
280{
281 if (qgcApp()->runningUnitTests()) {
282 return;
283 }
284
285 AppSettings *const appSettings = SettingsManager::instance()->appSettings();
286 if (appSettings->disableAllPersistence()->rawValue().toBool()) {
287 return;
288 }
289
290#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
291 if (!SettingsManager::instance()->mavlinkSettings()->telemetrySave()->rawValue().toBool()) {
292 return;
293 }
294#endif
295
296 if (_tempLogFile->isOpen()) {
297 return;
298 }
299
300 if (_logSuspendReplay) {
301 return;
302 }
303
304 // Generate unique temp file path for this logging session
305 const QString logPath = QGCFileHelper::uniqueTempPath(
306 QStringLiteral("%1.%2").arg(_tempLogFileTemplate, _logFileExtension));
307 if (logPath.isEmpty()) {
308 qCWarning(MAVLinkProtocolLog) << "Failed to generate temp log path";
309 _logSuspendError = true;
310 return;
311 }
312
313 _tempLogFile->setFileName(logPath);
314 if (!_tempLogFile->open(QIODevice::WriteOnly)) {
315 const QString message = QStringLiteral("Opening Flight Data file for writing failed. "
316 "Unable to write to %1. Please choose a different file location.")
317 .arg(_tempLogFile->fileName());
318 qgcApp()->showAppMessage(message, getName());
319 _closeLogFile();
320 _logSuspendError = true;
321 return;
322 }
323
324 qCDebug(MAVLinkProtocolLog) << "Temp log" << _tempLogFile->fileName();
325 (void) _checkTelemetrySavePath();
326
327 _logSuspendError = false;
328}
329
330void MAVLinkProtocol::_stopLogging()
331{
332 if (_tempLogFile->isOpen() && _closeLogFile()) {
333 auto appSettings = SettingsManager::instance()->appSettings();
334 auto mavlinkSettings = SettingsManager::instance()->mavlinkSettings();
335 if ((_vehicleWasArmed || mavlinkSettings->telemetrySaveNotArmed()->rawValue().toBool()) &&
336 mavlinkSettings->telemetrySave()->rawValue().toBool() &&
337 !appSettings->disableAllPersistence()->rawValue().toBool()) {
338 _saveTelemetryLog(_tempLogFile->fileName());
339 } else {
340 (void) QFile::remove(_tempLogFile->fileName());
341 }
342 }
343
344 _vehicleWasArmed = false;
345}
346
348{
349 static const QDir tempDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation));
350 static const QString filter(QStringLiteral("*.%1").arg(_logFileExtension));
351 static const QStringList filterList(filter);
352
353 const QFileInfoList fileInfoList = tempDir.entryInfoList(filterList, QDir::Files);
354 qCDebug(MAVLinkProtocolLog) << "Orphaned log file count" << fileInfoList.count();
355
356 for (const QFileInfo &fileInfo: fileInfoList) {
357 qCDebug(MAVLinkProtocolLog) << "Orphaned log file" << fileInfo.filePath();
358 if (fileInfo.size() == 0) {
359 (void) QFile::remove(fileInfo.filePath());
360 continue;
361 }
362 _saveTelemetryLog(fileInfo.filePath());
363 }
364}
365
367{
368 static const QDir tempDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation));
369 static const QString filter(QStringLiteral("*.%1").arg(_logFileExtension));
370
371 const QFileInfoList fileInfoList = tempDir.entryInfoList(QStringList(filter), QDir::Files);
372 qCDebug(MAVLinkProtocolLog) << "Temp log file count" << fileInfoList.count();
373
374 for (const QFileInfo &fileInfo: fileInfoList) {
375 qCDebug(MAVLinkProtocolLog) << "Temp log file" << fileInfo.filePath();
376 (void) QFile::remove(fileInfo.filePath());
377 }
378}
379
380void MAVLinkProtocol::_saveTelemetryLog(const QString &tempLogfile)
381{
382 if (_checkTelemetrySavePath()) {
383 const QString saveDirPath = SettingsManager::instance()->appSettings()->telemetrySavePath();
384 const QDir saveDir(saveDirPath);
385
386 const QString nameFormat("%1%2.%3");
387 const QString dtFormat("yyyy-MM-dd hh-mm-ss");
388
389 int tryIndex = 1;
390 QString saveFileName = nameFormat.arg(QDateTime::currentDateTime().toString(dtFormat), QString(), AppSettings::telemetryFileExtension);
391 while (saveDir.exists(saveFileName)) {
392 saveFileName = nameFormat.arg(QDateTime::currentDateTime().toString(dtFormat), QStringLiteral(".%1").arg(tryIndex++), AppSettings::telemetryFileExtension);
393 }
394
395 const QString saveFilePath = saveDir.absoluteFilePath(saveFileName);
396
397 QFile in(tempLogfile);
398 if (!in.open(QIODevice::ReadOnly)) {
399 const QString error = tr("Unable to save telemetry log. Error opening source '%1': '%2'.").arg(tempLogfile, in.errorString());
400 qgcApp()->showAppMessage(error);
401 (void) QFile::remove(tempLogfile);
402 return;
403 }
404
405 QSaveFile out(saveFilePath);
406 out.setDirectWriteFallback(true); // allows non-atomic fallback where rename isn’t possible
407
408 if (!out.open(QIODevice::WriteOnly)) {
409 const QString error = tr("Unable to save telemetry log. Error opening destination '%1': '%2'.").arg(saveFilePath, out.errorString());
410 qgcApp()->showAppMessage(error);
411 (void) QFile::remove(tempLogfile);
412 return;
413 }
414
415 // Stream copy to avoid large allocations.
416 QByteArray buffer;
417 constexpr int bufferSize = 256 * 1024; // 256 KiB
418 buffer.resize(bufferSize);
419 while (true) {
420 const qint64 n = in.read(buffer.data(), buffer.size());
421 if (n == 0) {
422 break;
423 }
424 if (n < 0) {
425 const QString error = tr("Unable to save telemetry log. Error reading source '%1': '%2'.").arg(tempLogfile, in.errorString());
426 qgcApp()->showAppMessage(error);
427 out.cancelWriting();
428 (void) QFile::remove(tempLogfile);
429 return;
430 }
431 if (out.write(buffer.constData(), n) != n) {
432 const QString error = tr("Unable to save telemetry log. Error writing destination '%1': '%2'.").arg(saveFilePath, out.errorString());
433 qgcApp()->showAppMessage(error);
434 out.cancelWriting();
435 (void) QFile::remove(tempLogfile);
436 return;
437 }
438 }
439
440 if (!out.commit()) {
441 const QString error = tr("Unable to finalize telemetry log '%1': '%2'.").arg(saveFilePath, out.errorString());
442 qgcApp()->showAppMessage(error);
443 (void) QFile::remove(tempLogfile);
444 return;
445 }
446
447 constexpr QFileDevice::Permissions perms =
448 QFileDevice::ReadOwner | QFileDevice::WriteOwner |
449 QFileDevice::ReadGroup |
450 QFileDevice::ReadOther;
451 (void) out.setPermissions(perms);
452 }
453
454 (void) QFile::remove(tempLogfile);
455}
456
457bool MAVLinkProtocol::_checkTelemetrySavePath()
458{
459 const QString saveDirPath = SettingsManager::instance()->appSettings()->telemetrySavePath();
460 if (saveDirPath.isEmpty()) {
461 const QString error = tr("Unable to save telemetry log. Application save directory is not set.");
462 qgcApp()->showAppMessage(error);
463 return false;
464 }
465
466 const QDir saveDir(saveDirPath);
467 if (!saveDir.exists()) {
468 const QString error = tr("Unable to save telemetry log. Telemetry save directory \"%1\" does not exist.").arg(saveDirPath);
469 qgcApp()->showAppMessage(error);
470 return false;
471 }
472
473 return true;
474}
475
476void MAVLinkProtocol::_vehicleCountChanged()
477{
478 if (MultiVehicleManager::instance()->vehicles()->count() == 0) {
479 _stopLogging();
480 }
481}
482
484{
485 return SettingsManager::instance()->mavlinkSettings()->gcsMavlinkSystemID()->rawValue().toInt();
486}
std::shared_ptr< LinkInterface > SharedLinkInterfacePtr
Q_APPLICATION_STATIC(MAVLinkProtocol, _mavlinkProtocolInstance)
#define qgcApp()
Error error
struct __mavlink_message mavlink_message_t
#define QGC_LOGGING_CATEGORY(name, categoryStr)
Application Settings.
Definition AppSettings.h:9
Fact *disableAllPersistence READ disableAllPersistence CONSTANT Fact * disableAllPersistence()
QString telemetrySavePath()
static constexpr const char * telemetryFileExtension
Definition AppSettings.h:91
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 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)
Message received and directly copied via signal.
void vehicleHeartbeatInfo(LinkInterface *link, int vehicleId, int componentId, int vehicleFirmwareType, int vehicleType)
Heartbeat received on link.
void logSentBytes(const LinkInterface *link, const QByteArray &data)
static MAVLinkProtocol * instance()
int getSystemId() const
Get the system id of this application.
void resetMetadataForLink(LinkInterface *link)
Reset the counters for all metadata for this link.
~MAVLinkProtocol()
Destructor for the MAVLinkProtocol class.
static QString getName()
Get the human-friendly name of this protocol.
static void deleteTempLogFiles()
Deletes any log files which are in the temp directory.
Fact *gcsMavlinkSystemID READ gcsMavlinkSystemID CONSTANT Fact * gcsMavlinkSystemID()
void vehicleRemoved(Vehicle *vehicle)
QString uniqueTempPath(const QString &templateName)