QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
MockLink.cc
Go to the documentation of this file.
1#include "MockLink.h"
2#include "MAVLinkLib.h"
3#include "LinkManager.h"
4#include "MAVLinkProtocol.h"
5#include "MAVLinkSigning.h"
6#include "SecureMemory.h"
7#include "MockLinkCamera.h"
8#include "MockLinkFTP.h"
9#include "MockLinkGimbal.h"
10#include "MockLinkWorker.h"
11#include "QGCLoggingCategory.h"
12#include "FirmwarePlugin.h"
13#include "FactMetaData.h"
14#include "ParameterManager.h"
15#include "AppMessages.h"
16#include "QGCMath.h"
17
18#ifdef QGC_GST_STREAMING
20#endif
21
22#include <QtCore/QFile>
23#include <QtCore/QJsonArray>
24#include <QtCore/QJsonDocument>
25#include <QtCore/QJsonObject>
26#include <QtCore/QMutexLocker>
27#include <QtCore/QSet>
28#include <QtCore/QRandomGenerator>
29#include <QtCore/QTemporaryFile>
30#include <QtCore/QThread>
31#include <QtCore/QTimer>
32
33#include <cmath>
34#include <cstring>
35
36QGC_LOGGING_CATEGORY(MockLinkLog, "Comms.MockLink.MockLink")
37QGC_LOGGING_CATEGORY(MockLinkVerboseLog, "Comms.MockLink.MockLink:verbose")
38
39std::atomic<int> MockLink::_nextVehicleSystemId{128};
40
41QList<MockLink::FlightMode_t> MockLink::_availableFlightModes = {
42 // Mode Name Standard Mode Custom Mode CanBeSet adv
43 { "Manual", 0, PX4CustomMode::MANUAL, true, true },
44 { "Stabilized", 0, PX4CustomMode::STABILIZED, true, true },
45 { "Acro", 0, PX4CustomMode::ACRO, true, true },
46 { "Altitude", 0, PX4CustomMode::ALTCTL, true, false},
47 { "Offboard", 0, PX4CustomMode::OFFBOARD, true, true },
48 { "Position", 0, PX4CustomMode::POSCTL_POSCTL, true, false},
49 { "Orbit", 0, PX4CustomMode::POSCTL_ORBIT, false, true },
50 { "Hold", 0, PX4CustomMode::AUTO_LOITER, true, true },
51 { "Mission", 0, PX4CustomMode::AUTO_MISSION, true, true },
52 { "Return", 0, PX4CustomMode::AUTO_RTL, true, true },
53 { "Land", MAV_STANDARD_MODE_LAND, PX4CustomMode::AUTO_LAND, false, true },
54 { "Precision Landing", 0, PX4CustomMode::AUTO_PRECLAND, true, true },
55 { "Takeoff", MAV_STANDARD_MODE_TAKEOFF, PX4CustomMode::AUTO_TAKEOFF, false, false},
56 { "MockLink Mode", 0, PX4CustomMode::RATTITUDE, true, false},
57 // Deliberately uses the reserved (deleted RTGS) AUTO sub-mode so QGC has no name for it
58 { "(Mode not available)", 0, static_cast<uint32_t>(PX4_CUSTOM_MAIN_MODE_AUTO << 16 | (PX4_CUSTOM_SUB_MODE_AUTO_RESERVED_DO_NOT_USE << 24)), false, false},
59 { "MockLink Mode (delayed)",0, PX4CustomMode::AUTO_FOLLOW_TARGET, true, false},
60};
61
63 : LinkInterface(config, parent)
64 , _mockConfig(qobject_cast<const MockConfiguration*>(_config.get()))
65 , _firmwareType(_mockConfig->firmwareType())
66 , _vehicleType(_mockConfig->vehicleType())
67 , _sendStatusText(_mockConfig->sendStatusText())
68 , _apmStartFreshParams(_mockConfig->apmStartFreshParams())
69 , _enableCamera(_mockConfig->enableCamera())
70 , _enableGimbal(_mockConfig->enableGimbal())
71 , _enableProximity(_mockConfig->enableProximity())
72 , _failureMode(_mockConfig->failureMode())
73 , _stayMavlinkV1(_mockConfig->stayMavlinkV1())
74 , _ftpCapability(_mockConfig->ftpCapability())
75 , _vehicleSystemId(_mockConfig->incrementVehicleId() ? _nextVehicleSystemId++ : static_cast<int>(_nextVehicleSystemId))
76 , _vehicleLatitude(_defaultVehicleLatitude + ((_vehicleSystemId - 128) * 0.0001))
77 , _vehicleLongitude(_defaultVehicleLongitude + ((_vehicleSystemId - 128) * 0.0001))
78 , _boardVendorId(_mockConfig->boardVendorId())
79 , _boardProductId(_mockConfig->boardProductId())
80 , _missionItemHandler(new MockLinkMissionItemHandler(this))
81 , _mockLinkCamera(_enableCamera ? new MockLinkCamera(this,
82 _mockConfig->cameraCaptureVideo(),
83 _mockConfig->cameraCaptureImage(),
84 _mockConfig->cameraHasModes(),
85 _mockConfig->cameraHasVideoStream(),
86 _mockConfig->cameraCanCaptureImageInVideoMode(),
87 _mockConfig->cameraCanCaptureVideoInImageMode(),
88 _mockConfig->cameraHasBasicZoom(),
89 _mockConfig->cameraHasTrackingPoint(),
90 _mockConfig->cameraHasTrackingRectangle())
91 : nullptr)
92 , _mockLinkGimbal(_enableGimbal ? new MockLinkGimbal(this,
93 _mockConfig->gimbalHasRollAxis(),
94 _mockConfig->gimbalHasPitchAxis(),
95 _mockConfig->gimbalHasYawAxis(),
96 _mockConfig->gimbalHasYawFollow(),
97 _mockConfig->gimbalHasYawLock(),
98 _mockConfig->gimbalHasRetract(),
99 _mockConfig->gimbalHasNeutral())
100 : nullptr)
101 , _mockLinkPX4Calibration(new MockLinkPX4Calibration(this))
102 , _mockLinkFTP(new MockLinkFTP(_vehicleSystemId, _vehicleComponentId, this))
103 , _requestedVideoStreamType(_mockConfig->videoStreamTypeEnum())
104{
105 qCDebug(MockLinkLog) << this;
106
107 if (_mockConfig->startArmed()) {
108 setArmed(true);
109 }
110
111 if (_mockConfig->preloadMission()) {
112 _missionItemHandler->loadSimpleMultirotorMission();
113 }
114
115 // Initialize ADS-B vehicles with different starting conditions
116 _adsbVehicles.reserve(_numberOfVehicles);
117 for (int i = 0; i < _numberOfVehicles; ++i) {
118 ADSBVehicle vehicle{};
119 vehicle.angle = i * 72.0; // Different starting directions (angles 0, 72, 144, 216, 288)
120
121 // Set initial coordinates slightly offset from the default coordinates
122 const double latOffset = 0.001 * i;
123 const double lonOffset = 0.001 * (i % 2 == 0 ? i : -i);
124 vehicle.coordinate = QGeoCoordinate(_defaultVehicleLatitude + latOffset, _defaultVehicleLongitude + lonOffset);
125
126 // Set a unique starting altitude for each vehicle near the home altitude
127 vehicle.altitude = _defaultVehicleHomeAltitude + (i * 5);
128
129 _adsbVehicles.append(vehicle);
130 }
131
132 (void) QObject::connect(this, &MockLink::writeBytesQueuedSignal, this, &MockLink::_writeBytesQueued, Qt::QueuedConnection);
133
134 _loadParams();
135 _runningTime.start();
136
137 _workerThread = new QThread(this);
138 _workerThread->setObjectName(QStringLiteral("Mock_%1").arg(_mockConfig->name()));
139 _worker = new MockLinkWorker(this);
140 _worker->moveToThread(_workerThread);
141 (void) connect(_workerThread, &QThread::started, _worker, &MockLinkWorker::startWork);
142 (void) connect(_workerThread, &QThread::finished, _worker, &QObject::deleteLater);
143 _workerThread->start();
144}
145
147{
149
150 delete _mockLinkCamera;
151 delete _mockLinkGimbal;
152 delete _mockLinkPX4Calibration;
153
154 if (!_logDownloadFilename.isEmpty()) {
155 QFile::remove(_logDownloadFilename);
156 }
157
158 if (_workerThread) {
159 _workerThread->quit();
160 _workerThread->wait();
161 }
162
163 qCDebug(MockLinkLog) << this;
164}
165
166bool MockLink::_connect()
167{
168 if (!_connected) {
169 _connected = true;
170 _disconnectedEmitted = false;
171 // ArduPilot starts out sending MAVLink v1 and only switches to v2 once it sees a v2
172 // message from the GCS. Emulate that: outgoing traffic starts as v1.
173 // High latency links are exempt: QGC doesn't transmit on them, so the upgrade could never happen.
174 // Note: high latency takes precedence over OptionStayMavlinkV1 (the two are not meant to be combined).
175 _mavlinkV2Upgraded = linkConfiguration()->isHighLatency();
176 mavlink_status_t *const outgoingStatus = mavlink_get_channel_status(_outgoingMavlinkChannel);
177 if (_mavlinkV2Upgraded) {
178 outgoingStatus->flags &= ~MAVLINK_STATUS_FLAG_OUT_MAVLINK1;
179 } else {
180 outgoingStatus->flags |= MAVLINK_STATUS_FLAG_OUT_MAVLINK1;
181 }
182 mavlink_status_t *const incomingStatus = mavlink_get_channel_status(_incomingMavlinkChannel);
183 incomingStatus->flags &= ~MAVLINK_STATUS_FLAG_OUT_MAVLINK1;
184
185 _startVideoStreamServer();
186
187 emit connected();
188 }
189
190 return true;
191}
192
194{
195 _missionItemHandler->shutdown();
196
197 // Stop worker thread first to prevent any more messages from being sent.
198 // This must happen before setting _connected = false to avoid race conditions
199 // where the worker checks _connected (true), then we set it false, then the
200 // worker continues sending messages to a disconnecting/destroyed vehicle.
201 if (_workerThread && _workerThread->isRunning()) {
202 _workerThread->quit();
203 _workerThread->wait();
204 }
205
206 // signing/streams pointers alias MockLink memory — clear before emit in case the disconnected signal frees us.
207 if (_outgoingMavlinkChannelIsSet()) {
208 mavlink_status_t* const outgoingStatus = mavlink_get_channel_status(_outgoingMavlinkChannel);
209 outgoingStatus->signing = nullptr;
210 outgoingStatus->signing_streams = nullptr;
211 mavlink_reset_channel_status(_outgoingMavlinkChannel);
212 }
213
214 // Must run before the disconnected emit below: that signal can release the last
215 // shared_ptr to this MockLink, so touching members afterwards is use-after-free.
216 _stopVideoStreamServer();
217
218 if (_connected) {
219 _connected = false;
220 if (!_disconnectedEmitted.exchange(true)) {
221 emit disconnected();
222 }
223 }
224}
225
226void MockLink::_startVideoStreamServer()
227{
228#ifdef QGC_GST_STREAMING
229 // Only serve a stream when a camera advertising a video stream is present and a type was requested.
230 if (_videoStreamServer || !_mockLinkCamera || !_mockConfig->cameraHasVideoStream()
231 || _requestedVideoStreamType == MockConfiguration::VideoStreamNone) {
232 return;
233 }
234
236 quint16 port = 5600;
237 switch (_requestedVideoStreamType) {
240 port = 5600;
241 break;
244 port = 5601;
245 break;
248 port = 8554;
249 break;
252 port = 5600;
253 break;
256 port = 5600;
257 break;
259 return;
260 }
261
262 auto *server = new MockVideoStreamServer();
263 if (!server->start(serverType, QStringLiteral("127.0.0.1"), port)) {
264 qCWarning(MockLinkLog) << "Failed to start mock video stream server for type" << _requestedVideoStreamType;
265 delete server;
266 return;
267 }
268
269 _videoStreamServer = server;
270 {
271 QMutexLocker locker(&_videoStreamMutex);
272 _servedVideoStreamType = _requestedVideoStreamType;
273 _videoStreamUri = server->servedUri();
274 }
275 qCDebug(MockLinkLog) << "Mock video stream server serving" << server->servedUri();
276#endif
277}
278
279void MockLink::_stopVideoStreamServer()
280{
281#ifdef QGC_GST_STREAMING
282 // Clear the served-stream snapshot first so observers (servedVideoStream) stop
283 // advertising the URI before the server actually goes away.
284 {
285 QMutexLocker locker(&_videoStreamMutex);
286 _servedVideoStreamType = MockConfiguration::VideoStreamNone;
287 _videoStreamUri.clear();
288 }
289 if (_videoStreamServer) {
290 delete _videoStreamServer;
291 _videoStreamServer = nullptr;
292 }
293#endif
294}
295
297{
298 QMutexLocker locker(&_videoStreamMutex);
299 type = _servedVideoStreamType;
300 uri = _videoStreamUri;
301}
302
304{
305 if (!_mavlinkStarted || !_connected || !mavlinkChannelIsSet()) {
306 return;
307 }
308
309 if (linkConfiguration()->isHighLatency() && _highLatencyTransmissionEnabled) {
310 _sendHighLatency2();
311 return;
312 }
313
314 _sendVibration();
315 _sendBatteryStatus();
316 _sendNamedValueFloats();
317 _sendSysStatus();
318 _sendADSBVehicles();
319 if (_vehicleType != MAV_TYPE_SUBMARINE) {
320 _sendRemoteIDArmStatus();
321 }
322 _sendAvailableModesMonitor();
323
324 if (_enableGimbal) {
325 _mockLinkGimbal->run1HzTasks();
326 }
327
328 if (_enableProximity) {
329 _sendDistanceSensors();
330 }
331
332 _sendEscInfo();
333 _sendEscStatus();
334 _sendRadioStatus();
335
336 if (_enableCamera) {
337 _mockLinkCamera->sendCameraHeartbeats();
338 }
339
340 if (!QGC::runningUnitTests()) {
341 // Sending RC Channels during unit test breaks RC tests which does it's own RC simulation
342 _sendRCChannels();
343 }
344
345 if (_sendHomePositionDelayCount > 0) {
346 // We delay home position for better testing
347 _sendHomePositionDelayCount--;
348 } else {
349 _sendHomePosition();
350 // We piggy back on this delay to signal we have new standard modes available
351 if (_availableModesMonitorSeqNumber == 0) {
352 qCDebug(MockLinkLog) << "Bumping sequence number for available modes monitor to trigger requery of modes";
353 _availableModesMonitorSeqNumber = 1;
354 }
355 }
356}
357
359{
360 if (linkConfiguration()->isHighLatency()) {
361 return;
362 }
363
364 if (_mavlinkStarted && _connected && mavlinkChannelIsSet()) {
365 _sendHeartBeat();
366 const bool gpsDelayExpired = (_sendGPSPositionDelayCount == 0);
367 if (_sendGPSPositionDelayCount > 0) {
368 // We delay gps position for better testing
369 _sendGPSPositionDelayCount--;
370 }
371 if (gpsDelayExpired || QGC::runningUnitTests()) {
372 if (_vehicleType != MAV_TYPE_SUBMARINE) {
373 _sendGpsRawInt();
374 _sendGlobalPositionInt();
375 }
376 _sendExtendedSysState();
377 }
378
379 _sendAttitudeQuaternion();
380 _sendAttitudeTarget();
381 _sendLocalPositionNed();
382 _sendPositionTargetLocalNed();
383
384 _mockLinkPX4Calibration->run10HzTasks();
385
386 if (_enableCamera) {
387 _mockLinkCamera->run10HzTasks();
388 }
389 }
390}
391
393{
394 if (linkConfiguration()->isHighLatency()) {
395 return;
396 }
397
398 if (_mavlinkStarted && _connected && mavlinkChannelIsSet()) {
399 const int paramSends = QGC::runningUnitTests() ? kTestParamRequestListBatch : 1;
400 for (int i = 0; i < paramSends; ++i) {
401 _paramRequestListWorker();
402 }
403 _logDownloadWorker();
404 _availableModesWorker();
405 _apmCompassCalWorker();
406 _apmAccelCalWorker();
407 }
408}
409
411{
412 _sendStatusTextMessages();
413}
414
415bool MockLink::_allocateMavlinkChannel()
416{
417 // should only be called by the LinkManager during setup
418 Q_ASSERT(!_incomingMavlinkChannelIsSet());
419 Q_ASSERT(!_outgoingMavlinkChannelIsSet());
420 Q_ASSERT(!mavlinkChannelIsSet());
421
423 qCWarning(MockLinkLog) << "LinkInterface::_allocateMavlinkChannel failed";
424 return false;
425 }
426
427 _incomingMavlinkChannel = LinkManager::instance()->allocateMavlinkChannel();
428 if (!_incomingMavlinkChannelIsSet()) {
429 qCWarning(MockLinkLog) << "_allocateMavlinkChannel aux failed";
431 return false;
432 }
433
434 _outgoingMavlinkChannel = LinkManager::instance()->allocateMavlinkChannel();
435 if (!_outgoingMavlinkChannelIsSet()) {
436 qCWarning(MockLinkLog) << "_allocateMavlinkChannel vehicle failed";
437 LinkManager::instance()->freeMavlinkChannel(_incomingMavlinkChannel);
439 return false;
440 }
441
442 qCDebug(MockLinkLog) << "_allocateMavlinkChannel aux:" << _incomingMavlinkChannel << "vehicle:" << _outgoingMavlinkChannel;
443 return true;
444}
445
446void MockLink::_freeMavlinkChannel()
447{
448 qCDebug(MockLinkLog) << "_freeMavlinkChannel aux:" << _incomingMavlinkChannel << "vehicle:" << _outgoingMavlinkChannel;
449 if (!_incomingMavlinkChannelIsSet()) {
450 Q_ASSERT(!_outgoingMavlinkChannelIsSet());
451 Q_ASSERT(!mavlinkChannelIsSet());
452 return;
453 }
454
455 if (_outgoingMavlinkChannelIsSet()) {
456 // Detach our _mockSigning before the channel is freed; the bytes back this struct in MockLink memory.
457 mavlink_reset_channel_status(_outgoingMavlinkChannel);
458 LinkManager::instance()->freeMavlinkChannel(_outgoingMavlinkChannel);
459 _outgoingMavlinkChannel = LinkManager::invalidMavlinkChannel();
460 }
461 mavlink_reset_channel_status(_incomingMavlinkChannel);
462 LinkManager::instance()->freeMavlinkChannel(_incomingMavlinkChannel);
463 _incomingMavlinkChannel = LinkManager::invalidMavlinkChannel();
465}
466
467bool MockLink::_incomingMavlinkChannelIsSet() const
468{
469 return (LinkManager::invalidMavlinkChannel() != _incomingMavlinkChannel);
470}
471
472bool MockLink::_outgoingMavlinkChannelIsSet() const
473{
474 return (LinkManager::invalidMavlinkChannel() != _outgoingMavlinkChannel);
475}
476
477void MockLink::_loadParams()
478{
479 QFile paramFile;
480 if (_firmwareType == MAV_AUTOPILOT_ARDUPILOTMEGA) {
481 if (_vehicleType == MAV_TYPE_FIXED_WING) {
482 paramFile.setFileName(":/FirmwarePlugin/APM/Plane.OfflineEditing.params");
483 } else if (_vehicleType == MAV_TYPE_SUBMARINE ) {
484 paramFile.setFileName(":/FirmwarePlugin/APM/Sub.OfflineEditing.params");
485 } else if (_vehicleType == MAV_TYPE_GROUND_ROVER ) {
486 paramFile.setFileName(":/FirmwarePlugin/APM/Rover.OfflineEditing.params");
487 } else {
488 paramFile.setFileName(":/FirmwarePlugin/APM/Copter.OfflineEditing.params");
489 }
490 } else if (_firmwareType == MAV_AUTOPILOT_GENERIC) {
491 paramFile.setFileName(":/MockLink/GenericMockLink.params");
492 } else {
493 paramFile.setFileName(":/MockLink/PX4MockLink.params");
494 }
495
496 const bool success = paramFile.open(QFile::ReadOnly);
497 Q_UNUSED(success);
498 Q_ASSERT(success);
499
500 QTextStream paramStream(&paramFile);
501 while (!paramStream.atEnd()) {
502 const QString line = paramStream.readLine();
503
504 if (line.startsWith("#")) {
505 continue;
506 }
507
508 const QStringList paramData = line.split("\t");
509 Q_ASSERT(paramData.count() == 5);
510
511 const int compId = paramData.at(1).toInt();
512 const QString paramName = paramData.at(2);
513 const QString valStr = paramData.at(3);
514 const uint paramType = paramData.at(4).toUInt();
515
516 QVariant paramValue;
517 switch (paramType) {
518 case MAV_PARAM_TYPE_REAL32:
519 paramValue = QVariant(valStr.toFloat());
520 break;
521 case MAV_PARAM_TYPE_UINT32:
522 paramValue = QVariant(valStr.toUInt());
523 break;
524 case MAV_PARAM_TYPE_INT32:
525 paramValue = QVariant(valStr.toInt());
526 break;
527 case MAV_PARAM_TYPE_UINT16:
528 paramValue = QVariant((quint16)valStr.toUInt());
529 break;
530 case MAV_PARAM_TYPE_INT16:
531 paramValue = QVariant((qint16)valStr.toInt());
532 break;
533 case MAV_PARAM_TYPE_UINT8:
534 paramValue = QVariant((quint8)valStr.toUInt());
535 break;
536 case MAV_PARAM_TYPE_INT8:
537 paramValue = QVariant((qint8)valStr.toUInt());
538 break;
539 default:
540 qCCritical(MockLinkVerboseLog) << "Unknown type" << paramType;
541 paramValue = QVariant(valStr.toInt());
542 break;
543 }
544
545 qCDebug(MockLinkVerboseLog) << "Loading param" << paramName << paramValue;
546
547 _mapParamName2Value[compId][paramName] = paramValue;
548 _mapParamName2MavParamType[compId][paramName] = static_cast<MAV_PARAM_TYPE>(paramType);
549 }
550
551 if ((_firmwareType == MAV_AUTOPILOT_ARDUPILOTMEGA) && _apmStartFreshParams) {
552 _applyAPMFreshFlashState();
553 }
554}
555
566void MockLink::_resetParamsToDefaults()
567{
568 if (_firmwareType == MAV_AUTOPILOT_ARDUPILOTMEGA) {
569 // ArduPilot firmware default for calibration-indicator parameters is 0 (uncalibrated).
570 // Resetting these causes QGC's APMSensorsComponent::setupComplete() to return false,
571 // which is the correct post-reset state on a real vehicle.
572 for (auto compIt = _mapParamName2Value.begin(); compIt != _mapParamName2Value.end(); ++compIt) {
573 for (auto paramIt = compIt.value().begin(); paramIt != compIt.value().end(); ++paramIt) {
574 if (kAPMCalOffsetParams.contains(paramIt.key())) {
575 paramIt.value() = QVariant(0.0f);
576 }
577 }
578 }
579 return;
580 }
581
582 if (_firmwareType != MAV_AUTOPILOT_PX4) {
583 qCWarning(MockLinkLog) << "Param reset to defaults not supported for firmware type" << _firmwareType;
584 return;
585 }
586
587 QFile metaDataFile(QStringLiteral(":/MockLink/Parameter.MetaData.json"));
588 if (!metaDataFile.open(QFile::ReadOnly)) {
589 qCWarning(MockLinkLog) << "Unable to open parameter metadata for reset" << metaDataFile.fileName();
590 return;
591 }
592
593 QJsonParseError parseError{};
594 const QJsonDocument doc = QJsonDocument::fromJson(metaDataFile.readAll(), &parseError);
595 if (parseError.error != QJsonParseError::NoError) {
596 qCWarning(MockLinkLog) << "Unable to parse parameter metadata for reset:" << parseError.errorString();
597 return;
598 }
599
600 QHash<QString, QVariant> defaults;
601 const QJsonArray parameters = doc.object().value(QStringLiteral("parameters")).toArray();
602 for (const QJsonValue &parameter : parameters) {
603 const QJsonObject paramObject = parameter.toObject();
604 if (paramObject.contains(QStringLiteral("default"))) {
605 defaults[paramObject.value(QStringLiteral("name")).toString()] = paramObject.value(QStringLiteral("default")).toVariant();
606 }
607 }
608
609 for (auto compIt = _mapParamName2Value.begin(); compIt != _mapParamName2Value.end(); ++compIt) {
610 const int compId = compIt.key();
611 for (auto paramIt = compIt.value().begin(); paramIt != compIt.value().end(); ++paramIt) {
612 const QString &paramName = paramIt.key();
613 if (!_resetSysAutostartOnParamReset && (paramName == QLatin1String("SYS_AUTOSTART"))) {
614 continue;
615 }
616 const auto defaultIt = defaults.constFind(paramName);
617 if (defaultIt == defaults.constEnd()) {
618 continue;
619 }
620 switch (_mapParamName2MavParamType[compId][paramName]) {
621 case MAV_PARAM_TYPE_REAL32:
622 paramIt.value() = QVariant(defaultIt->toFloat());
623 break;
624 case MAV_PARAM_TYPE_UINT32:
625 paramIt.value() = QVariant(defaultIt->toUInt());
626 break;
627 case MAV_PARAM_TYPE_INT32:
628 paramIt.value() = QVariant(defaultIt->toInt());
629 break;
630 case MAV_PARAM_TYPE_UINT16:
631 paramIt.value() = QVariant(static_cast<quint16>(defaultIt->toUInt()));
632 break;
633 case MAV_PARAM_TYPE_INT16:
634 paramIt.value() = QVariant(static_cast<qint16>(defaultIt->toInt()));
635 break;
636 case MAV_PARAM_TYPE_UINT8:
637 paramIt.value() = QVariant(static_cast<quint8>(defaultIt->toUInt()));
638 break;
639 case MAV_PARAM_TYPE_INT8:
640 paramIt.value() = QVariant(static_cast<qint8>(defaultIt->toInt()));
641 break;
642 default:
643 qCWarning(MockLinkLog) << "Param reset skipped unhandled type" << _mapParamName2MavParamType[compId][paramName] << paramName;
644 break;
645 }
646 }
647 }
648}
649
653void MockLink::_applyAPMFreshFlashState()
654{
655 // Compass and accel offsets to 0 (uncalibrated)
656 _resetParamsToDefaults();
657
658 auto &paramMap = _mapParamName2Value[MAV_COMP_ID_AUTOPILOT1];
659 const auto &paramTypeMap = _mapParamName2MavParamType[MAV_COMP_ID_AUTOPILOT1];
660
661 // Set an integer param preserving the storage type used by _loadParams()
662 auto setIntParam = [&paramMap, &paramTypeMap](const QString &paramName, int value) {
663 if (!paramMap.contains(paramName)) {
664 return;
665 }
666 switch (paramTypeMap.value(paramName)) {
667 case MAV_PARAM_TYPE_UINT32: paramMap[paramName] = QVariant(static_cast<quint32>(value)); break;
668 case MAV_PARAM_TYPE_INT32: paramMap[paramName] = QVariant(value); break;
669 case MAV_PARAM_TYPE_UINT16: paramMap[paramName] = QVariant(static_cast<quint16>(value)); break;
670 case MAV_PARAM_TYPE_INT16: paramMap[paramName] = QVariant(static_cast<qint16>(value)); break;
671 case MAV_PARAM_TYPE_UINT8: paramMap[paramName] = QVariant(static_cast<quint8>(value)); break;
672 case MAV_PARAM_TYPE_INT8: paramMap[paramName] = QVariant(static_cast<qint8>(value)); break;
673 default: paramMap[paramName] = QVariant(value); break;
674 }
675 };
676
677 // No airframe selected. Not all vehicle types have FRAME_CLASS (Plane/Sub don't).
678 setIntParam(QStringLiteral("FRAME_CLASS"), 0);
679
680 // Radio uncalibrated: RC min/max/trim at firmware defaults for the mapped attitude channels
681 const QStringList rcMapParams = {
682 QStringLiteral("RCMAP_ROLL"),
683 QStringLiteral("RCMAP_PITCH"),
684 QStringLiteral("RCMAP_YAW"),
685 QStringLiteral("RCMAP_THROTTLE"),
686 };
687 for (const QString &mapParam : rcMapParams) {
688 if (!paramMap.contains(mapParam)) {
689 continue;
690 }
691 const int channel = paramMap[mapParam].toInt();
692 setIntParam(QStringLiteral("RC%1_MIN").arg(channel), 1100);
693 setIntParam(QStringLiteral("RC%1_MAX").arg(channel), 1900);
694 setIntParam(QStringLiteral("RC%1_TRIM").arg(channel), 1500);
695 }
696}
697
698void MockLink::_sendHeartBeat()
699{
700 mavlink_message_t msg{};
701 (void) mavlink_msg_heartbeat_pack_chan(
702 _vehicleSystemId,
703 _vehicleComponentId,
704 _outgoingMavlinkChannel,
705 &msg,
706 _vehicleType, // MAV_TYPE
707 _firmwareType, // MAV_AUTOPILOT
708 _mavBaseMode, // MAV_MODE
709 _mavCustomMode, // custom mode
710 _mavState // MAV_STATE
711 );
713}
714
715void MockLink::_sendHighLatency2()
716{
717 qCDebug(MockLinkLog) << "Sending" << _mavCustomMode;
718
719 union px4_custom_mode px4_cm{};
720 px4_cm.data = _mavCustomMode;
721
722 mavlink_message_t msg{};
723 (void) mavlink_msg_high_latency2_pack_chan(
724 _vehicleSystemId,
725 _vehicleComponentId,
726 _outgoingMavlinkChannel,
727 &msg,
728 0, // timestamp
729 _vehicleType, // MAV_TYPE
730 _firmwareType, // MAV_AUTOPILOT
731 px4_cm.custom_mode_hl, // custom_mode
732 static_cast<int32_t>(_vehicleLatitude * 1E7),
733 static_cast<int32_t>(_vehicleLongitude * 1E7),
734 static_cast<int16_t>(_vehicleAltitudeAMSL),
735 static_cast<int16_t>(_vehicleAltitudeAMSL), // target_altitude,
736 0, // heading
737 0, // target_heading
738 0, // target_distance
739 0, // throttle
740 0, // airspeed
741 0, // airspeed_sp
742 0, // groundspeed
743 0, // windspeed,
744 0, // wind_heading
745 UINT8_MAX, // eph not known
746 UINT8_MAX, // epv not known
747 0, // temperature_air
748 0, // climb_rate
749 -1, // battery, do not use?
750 0, // wp_num
751 0, // failure_flags
752 0, 0, 0 // custom0, custom1, custom2
753 );
755}
756
757void MockLink::_sendSysStatus()
758{
759 mavlink_message_t msg{};
760 (void) mavlink_msg_sys_status_pack_chan(
761 _vehicleSystemId,
762 _vehicleComponentId,
763 _outgoingMavlinkChannel,
764 &msg,
765 MAV_SYS_STATUS_SENSOR_GPS, // onboard_control_sensors_present
766 0, // onboard_control_sensors_enabled
767 0, // onboard_control_sensors_health
768 250, // load
769 4200 * 4, // voltage_battery
770 8000, // current_battery
771 _battery1PctRemaining, // battery_remaining
772 0,0,0,0,0,0,0,0,0
773 );
775}
776
777void MockLink::_sendBatteryStatus()
778{
779 if (_battery1PctRemaining > 1) {
780 _battery1PctRemaining = static_cast<int8_t>(100 - (_runningTime.elapsed() / 1000));
781 _battery1TimeRemaining = static_cast<double>(_batteryMaxTimeRemaining) * (static_cast<double>(_battery1PctRemaining) / 100.0);
782 if (_battery1PctRemaining > 50) {
783 _battery1ChargeState = MAV_BATTERY_CHARGE_STATE_OK;
784 } else if (_battery1PctRemaining > 30) {
785 _battery1ChargeState = MAV_BATTERY_CHARGE_STATE_LOW;
786 } else if (_battery1PctRemaining > 20) {
787 _battery1ChargeState = MAV_BATTERY_CHARGE_STATE_CRITICAL;
788 } else {
789 _battery1ChargeState = MAV_BATTERY_CHARGE_STATE_EMERGENCY;
790 }
791 }
792
793 if (_battery2PctRemaining > 1) {
794 _battery2PctRemaining = static_cast<int8_t>(100 - ((_runningTime.elapsed() / 1000) / 2));
795 _battery2TimeRemaining = static_cast<double>(_batteryMaxTimeRemaining) * (static_cast<double>(_battery2PctRemaining) / 100.0);
796 if (_battery2PctRemaining > 50) {
797 _battery2ChargeState = MAV_BATTERY_CHARGE_STATE_OK;
798 } else if (_battery2PctRemaining > 30) {
799 _battery2ChargeState = MAV_BATTERY_CHARGE_STATE_LOW;
800 } else if (_battery2PctRemaining > 20) {
801 _battery2ChargeState = MAV_BATTERY_CHARGE_STATE_CRITICAL;
802 } else {
803 _battery2ChargeState = MAV_BATTERY_CHARGE_STATE_EMERGENCY;
804 }
805 }
806
807 mavlink_message_t msg{};
808 uint16_t rgVoltages[10]{};
809 uint16_t rgVoltagesNone[10]{};
810 uint16_t rgVoltagesExtNone[4]{};
811
812 for (size_t i = 0; i < std::size(rgVoltages); i++) {
813 rgVoltages[i] = UINT16_MAX;
814 rgVoltagesNone[i] = UINT16_MAX;
815 }
816 rgVoltages[0] = rgVoltages[1] = rgVoltages[2] = 4200;
817
818 (void) mavlink_msg_battery_status_pack_chan(
819 _vehicleSystemId,
820 _vehicleComponentId,
821 _outgoingMavlinkChannel,
822 &msg,
823 1, // battery id
824 MAV_BATTERY_FUNCTION_ALL,
825 MAV_BATTERY_TYPE_LIPO,
826 2100, // temp cdegC
827 rgVoltages,
828 600, // battery cA
829 100, // current consumed mAh
830 -1, // energy consumed not supported
831 _battery1PctRemaining,
832 _battery1TimeRemaining,
833 _battery1ChargeState,
834 rgVoltagesExtNone,
835 0, // MAV_BATTERY_MODE
836 0 // MAV_BATTERY_FAULT
837 );
839
840 (void) mavlink_msg_battery_status_pack_chan(
841 _vehicleSystemId,
842 _vehicleComponentId,
843 _outgoingMavlinkChannel,
844 &msg,
845 2, // battery id
846 MAV_BATTERY_FUNCTION_ALL,
847 MAV_BATTERY_TYPE_LIPO,
848 INT16_MAX, // temp cdegC
849 rgVoltagesNone,
850 600, // battery cA
851 100, // current consumed mAh
852 -1, // energy consumed not supported
853 _battery2PctRemaining,
854 _battery2TimeRemaining,
855 _battery2ChargeState,
856 rgVoltagesExtNone,
857 0, // MAV_BATTERY_MODE
858 0 // MAV_BATTERY_FAULT
859 );
861}
862
863void MockLink::_sendNamedValueFloats()
864{
865 const uint32_t timeBootMs = static_cast<uint32_t>(_runningTime.elapsed());
866
867 // Send two named float values with varying data to exercise Inspector instance separation
868 const float sinVal = static_cast<float>(std::sin(static_cast<double>(timeBootMs) / 1000.0));
869 const float cosVal = static_cast<float>(std::cos(static_cast<double>(timeBootMs) / 1000.0));
870
871 // NAMED_VALUE_FLOAT.name is a fixed 10-byte field; pack_chan memcpys 10 bytes unconditionally.
872 static constexpr char kSinName[10] = "sin_wave";
873 static constexpr char kCosName[10] = "cos_wave";
874
875 mavlink_message_t msg{};
876 (void) mavlink_msg_named_value_float_pack_chan(
877 _vehicleSystemId,
878 _vehicleComponentId,
879 _outgoingMavlinkChannel,
880 &msg,
881 timeBootMs,
882 kSinName,
883 sinVal
884 );
886
887 (void) mavlink_msg_named_value_float_pack_chan(
888 _vehicleSystemId,
889 _vehicleComponentId,
890 _outgoingMavlinkChannel,
891 &msg,
892 timeBootMs,
893 kCosName,
894 cosVal
895 );
897}
898
899void MockLink::_sendVibration()
900{
901 mavlink_message_t msg{};
902 (void) mavlink_msg_vibration_pack_chan(
903 _vehicleSystemId,
904 _vehicleComponentId,
905 _outgoingMavlinkChannel,
906 &msg,
907 0, // time_usec
908 50.5, // vibration_x,
909 10.5, // vibration_y,
910 60.0, // vibration_z,
911 1, // clipping_0
912 2, // clipping_0
913 3 // clipping_0
914 );
916}
917
918void MockLink::_sendDistanceSensors()
919{
920 // Simulated proximity sensor ring: one DISTANCE_SENSOR message per yaw orientation.
921 // Two orientations are deliberately never sent so the UI shows sectors with no data.
922 static constexpr MAV_SENSOR_ORIENTATION rgOrientations[] = {
923 MAV_SENSOR_ROTATION_NONE,
924 MAV_SENSOR_ROTATION_YAW_45,
925 MAV_SENSOR_ROTATION_YAW_90,
926 MAV_SENSOR_ROTATION_YAW_180,
927 MAV_SENSOR_ROTATION_YAW_270,
928 MAV_SENSOR_ROTATION_YAW_315,
929 };
930
931 static constexpr uint16_t minDistanceCm = 20;
932 static constexpr uint16_t maxDistanceCm = 4000;
933
934 const uint32_t timeBootMs = static_cast<uint32_t>(_runningTime.elapsed());
935 const float quaternion[4]{};
936
937 for (size_t i = 0; i < std::size(rgOrientations); i++) {
938 // Slow sweep between 5m and 35m, phase shifted per array entry so the arcs move independently
939 const double sweep = std::sin((timeBootMs / 5000.0) + (i * M_PI / 4));
940 const uint16_t currentDistanceCm = static_cast<uint16_t>(2000 + (1500 * sweep));
941
942 mavlink_message_t msg{};
943 (void) mavlink_msg_distance_sensor_pack_chan(
944 _vehicleSystemId,
945 _vehicleComponentId,
946 _outgoingMavlinkChannel,
947 &msg,
948 timeBootMs,
949 minDistanceCm,
950 maxDistanceCm,
951 currentDistanceCm,
952 MAV_DISTANCE_SENSOR_LASER,
953 static_cast<uint8_t>(i), // id
954 rgOrientations[i],
955 255, // covariance - unknown
956 0.0f, // horizontal_fov - unknown
957 0.0f, // vertical_fov - unknown
958 quaternion, // valid only for MAV_SENSOR_ROTATION_CUSTOM
959 0 // signal_quality - unknown
960 );
962 }
963}
964
966{
967 if (!_commLost) {
968 uint8_t buffer[MAVLINK_MAX_PACKET_LEN]{};
969 const int cBuffer = mavlink_msg_to_send_buffer(buffer, &msg);
970 const QByteArray bytes(reinterpret_cast<char*>(buffer), cBuffer);
971 emit bytesReceived(this, bytes);
972 }
973}
974
975void MockLink::_writeBytes(const QByteArray &bytes)
976{
977 // This prevents the responses to mavlink messages from being sent until the _writeBytes returns.
978 emit writeBytesQueuedSignal(bytes);
979}
980
981void MockLink::_writeBytesQueued(const QByteArray &bytes)
982{
983 if (!_connected || !mavlinkChannelIsSet()) {
984 qCDebug(MockLinkLog) << "Dropping queued bytes on disconnected/uninitialized mock link";
985 return;
986 }
987
988 if (_inNSH) {
989 _handleIncomingNSHBytes(bytes.constData(), bytes.length());
990 return;
991 }
992
993 if (bytes.startsWith(QByteArrayLiteral("\r\r\r"))) {
994 _inNSH = true;
995 _handleIncomingNSHBytes(&bytes.constData()[3], bytes.length() - 3);
996 }
997
998 _handleIncomingMavlinkBytes(reinterpret_cast<const uint8_t*>(bytes.constData()), bytes.length());
999}
1000
1001void MockLink::_handleIncomingNSHBytes(const char *bytes, int cBytes)
1002{
1003 Q_UNUSED(cBytes);
1004
1005 // Drop back out of NSH
1006 if ((cBytes == 4) && (bytes[0] == '\r') && (bytes[1] == '\r') && (bytes[2] == '\r')) {
1007 _inNSH = false;
1008 return;
1009 }
1010
1011 if (cBytes > 0) {
1012 qCDebug(MockLinkLog) << "NSH:" << bytes;
1013#if 0
1014 // MockLink not quite ready to handle this correctly yet
1015 if (strncmp(bytes, "sh /etc/init.d/rc.usb\n", cBytes) == 0) {
1016 // This is the mavlink start command
1017 _mavlinkStarted = true;
1018 }
1019#endif
1020 }
1021}
1022
1023void MockLink::_handleIncomingMavlinkBytes(const uint8_t *bytes, int cBytes)
1024{
1025 mavlink_message_t msg{};
1026 mavlink_status_t comm{};
1027
1028 QMutexLocker lock(&_incomingMavlinkMutex);
1029 for (qint64 i = 0; i < cBytes; i++) {
1030 const int parsed = mavlink_parse_char(_incomingMavlinkChannel, bytes[i], &msg, &comm);
1031 if (!parsed) {
1032 continue;
1033 }
1034 if (!_mavlinkV2Upgraded && !_stayMavlinkV1 && (msg.magic == MAVLINK_STX)) {
1035 // First v2 message from GCS: switch outgoing traffic to v2, same as ArduPilot does.
1036 _mavlinkV2Upgraded = true;
1037 mavlink_status_t *const outgoingStatus = mavlink_get_channel_status(_outgoingMavlinkChannel);
1038 outgoingStatus->flags &= ~MAVLINK_STATUS_FLAG_OUT_MAVLINK1;
1039 qCDebug(MockLinkLog) << "Received MAVLink v2 message from GCS, upgrading outgoing traffic to v2";
1040 }
1041 lock.unlock();
1042 _handleIncomingMavlinkMsg(msg);
1043 lock.relock();
1044 }
1045}
1046
1047void MockLink::_updateIncomingMessageCounts(const mavlink_message_t &msg)
1048{
1049 _receivedMavlinkMessageCountMap[msg.msgid]++;
1050 _lastReceivedMavlinkMessageMap[msg.msgid] = msg;
1051
1052 // Update command-specific counts if this is a COMMAND_LONG message
1053 if (msg.msgid == MAVLINK_MSG_ID_COMMAND_LONG) {
1054 mavlink_command_long_t request{};
1055 mavlink_msg_command_long_decode(&msg, &request);
1056
1057 _receivedMavCommandCountMap[static_cast<MAV_CMD>(request.command)]++;
1058 _receivedMavCommandByCompCountMap[static_cast<MAV_CMD>(request.command)][request.target_component]++;
1059
1060 if (request.command == MAV_CMD_REQUEST_MESSAGE) {
1061 _receivedRequestMessageCountMap[static_cast<uint32_t>(request.param1)]++;
1062 _receivedRequestMessageByCompAndMsgCountMap[request.target_component][static_cast<int>(request.param1)]++;
1063 }
1064 } else if (msg.msgid == MAVLINK_MSG_ID_COMMAND_INT) {
1065 mavlink_command_int_t request{};
1066 mavlink_msg_command_int_decode(&msg, &request);
1067
1068 _receivedMavCommandCountMap[static_cast<MAV_CMD>(request.command)]++;
1069 _receivedMavCommandByCompCountMap[static_cast<MAV_CMD>(request.command)][request.target_component]++;
1070 }
1071}
1072
1073void MockLink::_handleIncomingMavlinkMsg(const mavlink_message_t &msg)
1074{
1075 _updateIncomingMessageCounts(msg);
1076
1077 if (_missionItemHandler->handleMavlinkMessage(msg)) {
1078 return;
1079 }
1080
1081 if (_enableCamera && _mockLinkCamera->handleMavlinkMessage(msg)) {
1082 return;
1083 }
1084
1085 if (_enableGimbal && _mockLinkGimbal->handleMavlinkMessage(msg)) {
1086 return;
1087 }
1088
1089 switch (msg.msgid) {
1090 case MAVLINK_MSG_ID_HEARTBEAT:
1091 _handleHeartBeat(msg);
1092 break;
1093 case MAVLINK_MSG_ID_PARAM_REQUEST_LIST:
1094 _handleParamRequestList(msg);
1095 break;
1096 case MAVLINK_MSG_ID_SET_MODE:
1097 _handleSetMode(msg);
1098 break;
1099 case MAVLINK_MSG_ID_PARAM_SET:
1100 _handleParamSet(msg);
1101 break;
1102 case MAVLINK_MSG_ID_PARAM_REQUEST_READ:
1103 _handleParamRequestRead(msg);
1104 break;
1105 case MAVLINK_MSG_ID_FILE_TRANSFER_PROTOCOL:
1106 _handleFTP(msg);
1107 break;
1108 case MAVLINK_MSG_ID_COMMAND_LONG:
1109 _handleCommandLong(msg);
1110 break;
1111 case MAVLINK_MSG_ID_COMMAND_INT:
1112 _handleCommandInt(msg);
1113 break;
1114 case MAVLINK_MSG_ID_MANUAL_CONTROL:
1115 _handleManualControl(msg);
1116 break;
1117 case MAVLINK_MSG_ID_RC_CHANNELS_OVERRIDE:
1118 _handleRCChannelsOverride(msg);
1119 break;
1120 case MAVLINK_MSG_ID_LOG_REQUEST_LIST:
1121 _handleLogRequestList(msg);
1122 break;
1123 case MAVLINK_MSG_ID_LOG_REQUEST_DATA:
1124 _handleLogRequestData(msg);
1125 break;
1126 case MAVLINK_MSG_ID_LOG_ERASE:
1127 _handleLogErase(msg);
1128 break;
1129 case MAVLINK_MSG_ID_PARAM_MAP_RC:
1130 _handleParamMapRC(msg);
1131 break;
1132 case MAVLINK_MSG_ID_SETUP_SIGNING:
1133 _handleSetupSigning(msg);
1134 break;
1135 case MAVLINK_MSG_ID_COMMAND_ACK:
1136 // GCS sends COMMAND_ACK(command=0) via nextClicked() to acknowledge each pose
1137 // during APM full accel calibration (the "Next" button press).
1138 if (_firmwareType == MAV_AUTOPILOT_ARDUPILOTMEGA) {
1140 mavlink_msg_command_ack_decode(&msg, &ack);
1141 if (ack.command == 0) {
1142 QMutexLocker locker(&_apmAccelCalMutex);
1143 if (_apmAccelCalPosIndex >= 0 && _apmAccelCalPosIndex < 6) {
1144 _apmAccelCalGotAck = true;
1145 }
1146 }
1147 }
1148 break;
1149 default:
1150 break;
1151 }
1152}
1153
1154void MockLink::_handleHeartBeat(const mavlink_message_t &msg)
1155{
1156 Q_UNUSED(msg);
1157 qCDebug(MockLinkVerboseLog) << "Heartbeat";
1158}
1159
1160void MockLink::_handleParamMapRC(const mavlink_message_t &msg)
1161{
1162 mavlink_param_map_rc_t paramMapRC{};
1163 mavlink_msg_param_map_rc_decode(&msg, &paramMapRC);
1164
1165 const QString paramName(QString::fromLocal8Bit(paramMapRC.param_id, static_cast<int>(strnlen(paramMapRC.param_id, MAVLINK_MSG_PARAM_MAP_RC_FIELD_PARAM_ID_LEN))));
1166
1167 if (paramMapRC.param_index == -1) {
1168 qCDebug(MockLinkLog) << QStringLiteral("MockLink - PARAM_MAP_RC: param(%1) tuningID(%2) centerValue(%3) scale(%4) min(%5) max(%6)").arg(paramName).arg(paramMapRC.parameter_rc_channel_index).arg(paramMapRC.param_value0).arg(paramMapRC.scale).arg(paramMapRC.param_value_min).arg(paramMapRC.param_value_max);
1169 } else if (paramMapRC.param_index == -2) {
1170 qCDebug(MockLinkLog) << "MockLink - PARAM_MAP_RC: Clear tuningID" << paramMapRC.parameter_rc_channel_index;
1171 } else {
1172 qCWarning(MockLinkLog) << "MockLink - PARAM_MAP_RC: Unsupported param_index" << paramMapRC.param_index;
1173 }
1174}
1175
1176void MockLink::_handleSetupSigning(const mavlink_message_t &msg)
1177{
1178 mavlink_setup_signing_t setupSigning{};
1179 mavlink_msg_setup_signing_decode(&msg, &setupSigning);
1180
1181 if (setupSigning.target_system != _vehicleSystemId) {
1182 return;
1183 }
1184
1185 // All-zero key = disable signing
1186 bool allZeroKey = true;
1187 for (const uint8_t byte : setupSigning.secret_key) {
1188 if (byte != 0) {
1189 allZeroKey = false;
1190 break;
1191 }
1192 }
1193
1194 _signingEnabled = !allZeroKey;
1195
1196 // Write C-state directly; routing through SigningController would clobber its _keyHint mid-confirmation.
1197 mavlink_status_t* const status = mavlink_get_channel_status(_outgoingMavlinkChannel);
1198 if (_signingEnabled) {
1199 memcpy(_mockSigning.secret_key, setupSigning.secret_key, sizeof(_mockSigning.secret_key));
1200 _mockSigning.link_id = _outgoingMavlinkChannel;
1201 _mockSigning.flags = MAVLINK_SIGNING_FLAG_SIGN_OUTGOING;
1202 _mockSigning.timestamp = MAVLinkSigning::currentSigningTimestampTicks();
1203 _mockSigning.accept_unsigned_callback = MAVLinkSigning::insecureConnectionAcceptUnsignedCallback;
1204 status->signing = &_mockSigning;
1205 status->signing_streams = &_mockSigningStreams;
1206 } else {
1207 QGC::secureZero(_mockSigning.secret_key, sizeof(_mockSigning.secret_key));
1208 _mockSigning.accept_unsigned_callback = nullptr;
1209 status->signing = nullptr;
1210 status->signing_streams = nullptr;
1211 }
1212
1213 qCDebug(MockLinkLog) << "Signing" << (_signingEnabled ? "enabled" : "disabled");
1214}
1215
1216void MockLink::_handleSetMode(const mavlink_message_t &msg)
1217{
1218 mavlink_set_mode_t request{};
1219 mavlink_msg_set_mode_decode(&msg, &request);
1220
1221 Q_ASSERT(request.target_system == _vehicleSystemId);
1222
1223 _mavBaseMode = request.base_mode;
1224 _mavCustomMode = request.custom_mode;
1225}
1226
1227void MockLink::_handleManualControl(const mavlink_message_t &msg)
1228{
1229 mavlink_manual_control_t manualControl{};
1230 mavlink_msg_manual_control_decode(&msg, &manualControl);
1231
1232 // INT16_MAX means "axis invalid/not provided" per MAVLink spec
1233 const auto axisStr = [](int16_t v) -> QString {
1234 return (v == INT16_MAX) ? QStringLiteral("invalid") : QString::number(v);
1235 };
1236 // Extension fields are only valid when the corresponding enabled_extensions bit is set
1237 const auto extStr = [](int16_t v, bool enabled) -> QString {
1238 return enabled ? QString::number(v) : QStringLiteral("disabled");
1239 };
1240
1241 const uint8_t ext = manualControl.enabled_extensions;
1242
1243 qCDebug(MockLinkVerboseLog).noquote()
1244 << "MANUAL_CONTROL"
1245 << "target:" << manualControl.target
1246 << "x:" << axisStr(manualControl.x)
1247 << "y:" << axisStr(manualControl.y)
1248 << "z:" << axisStr(manualControl.z)
1249 << "r:" << axisStr(manualControl.r)
1250 << "buttons:" << QStringLiteral("0x%1").arg(manualControl.buttons, 4, 16, QLatin1Char('0'))
1251 << "buttons2:" << QStringLiteral("0x%1").arg(manualControl.buttons2, 4, 16, QLatin1Char('0'))
1252 << "enabled_extensions:" << QStringLiteral("0x%1").arg(ext, 2, 16, QLatin1Char('0'))
1253 << "s(pitch):" << extStr(manualControl.s, ext & (1 << 0))
1254 << "t(roll):" << extStr(manualControl.t, ext & (1 << 1))
1255 << "aux1:" << extStr(manualControl.aux1, ext & (1 << 2))
1256 << "aux2:" << extStr(manualControl.aux2, ext & (1 << 3))
1257 << "aux3:" << extStr(manualControl.aux3, ext & (1 << 4))
1258 << "aux4:" << extStr(manualControl.aux4, ext & (1 << 5))
1259 << "aux5:" << extStr(manualControl.aux5, ext & (1 << 6))
1260 << "aux6:" << extStr(manualControl.aux6, ext & (1 << 7));
1261}
1262
1263void MockLink::_handleRCChannelsOverride(const mavlink_message_t &msg)
1264{
1265 mavlink_rc_channels_override_t override{};
1266 mavlink_msg_rc_channels_override_decode(&msg, &override);
1267
1268 // Per the MAVLink spec:
1269 // Channels 1-8: UINT16_MAX = ignore (no state change), 0 = release back to RC radio
1270 // Channels 9-18: UINT16_MAX or 0 = ignore, UINT16_MAX-1 = release back to RC radio
1271 const uint16_t rawValues[18] = {
1272 override.chan1_raw, override.chan2_raw, override.chan3_raw, override.chan4_raw,
1273 override.chan5_raw, override.chan6_raw, override.chan7_raw, override.chan8_raw,
1274 override.chan9_raw, override.chan10_raw, override.chan11_raw, override.chan12_raw,
1275 override.chan13_raw, override.chan14_raw, override.chan15_raw, override.chan16_raw,
1276 override.chan17_raw, override.chan18_raw,
1277 };
1278
1279 bool anyChange = false;
1280 for (int i = 0; i < kRcChannelOverrideChannelCount; ++i) {
1281 const uint16_t raw = rawValues[i];
1282 const bool isExtended = (i >= 8);
1283
1284 RCChannelOverride::State newState;
1285 if (isExtended) {
1286 if (raw == 0 || raw == UINT16_MAX) {
1287 continue; // ignore — no change to this channel's state
1288 } else if (raw == static_cast<uint16_t>(UINT16_MAX - 1)) {
1289 newState = RCChannelOverride::State::Released;
1290 } else {
1291 newState = RCChannelOverride::State::Overridden;
1292 }
1293 } else {
1294 if (raw == UINT16_MAX) {
1295 continue; // ignore — no change to this channel's state
1296 } else if (raw == 0) {
1297 newState = RCChannelOverride::State::Released;
1298 } else {
1299 newState = RCChannelOverride::State::Overridden;
1300 }
1301 }
1302
1303 RCChannelOverride &ch = _rcChannelOverrides[i];
1304 if (ch.state == newState) {
1305 continue;
1306 }
1307
1308 anyChange = true;
1309
1310 const auto stateLabel = [](RCChannelOverride::State s) -> const char * {
1311 switch (s) {
1312 case RCChannelOverride::State::Ignore: return "ignore";
1313 case RCChannelOverride::State::Released: return "released";
1314 case RCChannelOverride::State::Overridden: return "overridden";
1315 }
1316 return "unknown";
1317 };
1318 qCDebug(MockLinkLog).noquote() << QStringLiteral("RC_CHANNELS_OVERRIDE ch%1: %2 -> %3").arg(i + 1).arg(stateLabel(ch.state)).arg(stateLabel(newState));
1319
1320 ch.state = newState;
1321 ch.value = (newState == RCChannelOverride::State::Overridden) ? raw : 0;
1322 }
1323
1324 if (anyChange) {
1325 QStringList active;
1326 for (int i = 0; i < kRcChannelOverrideChannelCount; ++i) {
1327 if (_rcChannelOverrides[i].state == RCChannelOverride::State::Overridden) {
1328 active << QStringLiteral("ch%1").arg(i + 1);
1329 }
1330 }
1331 if (active.isEmpty()) {
1332 qCDebug(MockLinkLog) << "RC_CHANNELS_OVERRIDE: no channels currently overridden";
1333 } else {
1334 qCDebug(MockLinkLog).noquote() << "RC_CHANNELS_OVERRIDE active overrides:" << active.join(QStringLiteral(", "));
1335 }
1336 }
1337
1338 for (int i = 0; i < kRcChannelOverrideChannelCount; ++i) {
1339 if (_rcChannelOverrides[i].state == RCChannelOverride::State::Overridden) {
1340 qCDebug(MockLinkVerboseLog).noquote() << QStringLiteral("RC_CHANNELS_OVERRIDE ch%1 value: %2").arg(i + 1).arg(_rcChannelOverrides[i].value);
1341 }
1342 }
1343}
1344
1345void MockLink::_setParamFloatUnionIntoMap(int componentId, const QString &paramName, float paramFloat)
1346{
1347 Q_ASSERT(_mapParamName2Value.contains(componentId));
1348 Q_ASSERT(_mapParamName2Value[componentId].contains(paramName));
1349 Q_ASSERT(_mapParamName2MavParamType[componentId].contains(paramName));
1350
1351 const MAV_PARAM_TYPE paramType = _mapParamName2MavParamType[componentId][paramName];
1352 QVariant paramVariant;
1353 mavlink_param_union_t valueUnion{};
1354 valueUnion.param_float = paramFloat;
1355 switch (paramType) {
1356 case MAV_PARAM_TYPE_REAL32:
1357 paramVariant = QVariant::fromValue(valueUnion.param_float);
1358 break;
1359 case MAV_PARAM_TYPE_UINT32:
1360 paramVariant = QVariant::fromValue(valueUnion.param_uint32);
1361 break;
1362 case MAV_PARAM_TYPE_INT32:
1363 paramVariant = QVariant::fromValue(valueUnion.param_int32);
1364 break;
1365 case MAV_PARAM_TYPE_UINT16:
1366 paramVariant = QVariant::fromValue(valueUnion.param_uint16);
1367 break;
1368 case MAV_PARAM_TYPE_INT16:
1369 paramVariant = QVariant::fromValue(valueUnion.param_int16);
1370 break;
1371 case MAV_PARAM_TYPE_UINT8:
1372 paramVariant = QVariant::fromValue(valueUnion.param_uint8);
1373 break;
1374 case MAV_PARAM_TYPE_INT8:
1375 paramVariant = QVariant::fromValue(valueUnion.param_int8);
1376 break;
1377 default:
1378 qCCritical(MockLinkLog) << "Invalid parameter type" << paramType;
1379 paramVariant = QVariant::fromValue(valueUnion.param_int32);
1380 break;
1381 }
1382
1383 qCDebug(MockLinkLog) << "_setParamFloatUnionIntoMap" << paramName << paramVariant;
1384 _mapParamName2Value[componentId][paramName] = paramVariant;
1385}
1386
1387void MockLink::setMockParamValue(int componentId, const QString &paramName, float value)
1388{
1389 mavlink_param_union_t valueUnion{};
1390 valueUnion.param_float = value;
1391 _setParamFloatUnionIntoMap(componentId, paramName, valueUnion.param_float);
1392}
1393
1394float MockLink::_floatUnionForParam(int componentId, const QString &paramName)
1395{
1396 Q_ASSERT(_mapParamName2Value.contains(componentId));
1397 Q_ASSERT(_mapParamName2Value[componentId].contains(paramName));
1398 Q_ASSERT(_mapParamName2MavParamType[componentId].contains(paramName));
1399
1400 const MAV_PARAM_TYPE paramType = _mapParamName2MavParamType[componentId][paramName];
1401 const QVariant paramVar = _mapParamName2Value[componentId][paramName];
1402
1403 mavlink_param_union_t valueUnion{};
1404 switch (paramType) {
1405 case MAV_PARAM_TYPE_REAL32:
1406 valueUnion.param_float = paramVar.toFloat();
1407 break;
1408 case MAV_PARAM_TYPE_UINT32:
1409 if (_firmwareType == MAV_AUTOPILOT_ARDUPILOTMEGA) {
1410 valueUnion.param_float = paramVar.toUInt();
1411 } else {
1412 valueUnion.param_uint32 = paramVar.toUInt();
1413 }
1414 break;
1415 case MAV_PARAM_TYPE_INT32:
1416 if (_firmwareType == MAV_AUTOPILOT_ARDUPILOTMEGA) {
1417 valueUnion.param_float = paramVar.toInt();
1418 } else {
1419 valueUnion.param_int32 = paramVar.toInt();
1420 }
1421 break;
1422 case MAV_PARAM_TYPE_UINT16:
1423 if (_firmwareType == MAV_AUTOPILOT_ARDUPILOTMEGA) {
1424 valueUnion.param_float = paramVar.toUInt();
1425 } else {
1426 valueUnion.param_uint16 = paramVar.toUInt();
1427 }
1428 break;
1429 case MAV_PARAM_TYPE_INT16:
1430 if (_firmwareType == MAV_AUTOPILOT_ARDUPILOTMEGA) {
1431 valueUnion.param_float = paramVar.toInt();
1432 } else {
1433 valueUnion.param_int16 = paramVar.toInt();
1434 }
1435 break;
1436 case MAV_PARAM_TYPE_UINT8:
1437 if (_firmwareType == MAV_AUTOPILOT_ARDUPILOTMEGA) {
1438 valueUnion.param_float = paramVar.toUInt();
1439 } else {
1440 valueUnion.param_uint8 = paramVar.toUInt();
1441 }
1442 break;
1443 case MAV_PARAM_TYPE_INT8:
1444 if (_firmwareType == MAV_AUTOPILOT_ARDUPILOTMEGA) {
1445 valueUnion.param_float = (unsigned char)paramVar.toChar().toLatin1();
1446 } else {
1447 valueUnion.param_int8 = (unsigned char)paramVar.toChar().toLatin1();
1448 }
1449 break;
1450 default:
1451 if (_firmwareType == MAV_AUTOPILOT_ARDUPILOTMEGA) {
1452 valueUnion.param_float = paramVar.toInt();
1453 } else {
1454 valueUnion.param_int32 = paramVar.toInt();
1455 }
1456 qCCritical(MockLinkLog) << "Invalid parameter type" << paramType;
1457 }
1458
1459 return valueUnion.param_float;
1460}
1461
1462uint32_t MockLink::_computeParamHash(int componentId) const
1463{
1464 // Volatile parameters are excluded from the hash, matching PX4 firmware and
1465 // ParameterManager::_tryCacheHashLoad. The list comes from the same metadata
1466 // json served to QGC so the two sides always agree.
1467 static const QSet<QString> volatileParams = []() {
1468 QSet<QString> volatiles;
1469 QFile metaDataFile(QStringLiteral(":/MockLink/Parameter.MetaData.json"));
1470 if (metaDataFile.open(QFile::ReadOnly)) {
1471 QJsonParseError parseError{};
1472 const QJsonDocument doc = QJsonDocument::fromJson(metaDataFile.readAll(), &parseError);
1473 if (parseError.error != QJsonParseError::NoError) {
1474 qCWarning(MockLinkLog) << "Unable to parse parameter metadata for volatile param list:" << parseError.errorString();
1475 }
1476 const QJsonArray parameters = doc.object().value(QStringLiteral("parameters")).toArray();
1477 for (const QJsonValue &parameter : parameters) {
1478 const QJsonObject paramObject = parameter.toObject();
1479 if (paramObject.value(QStringLiteral("volatile")).toBool()) {
1480 volatiles.insert(paramObject.value(QStringLiteral("name")).toString());
1481 }
1482 }
1483 } else {
1484 qCWarning(MockLinkLog) << "Unable to open parameter metadata for volatile param list" << metaDataFile.fileName();
1485 }
1486 return volatiles;
1487 }();
1488
1489 uint32_t crc = 0;
1490 const auto &params = _mapParamName2Value[componentId];
1491 for (auto it = params.constBegin(); it != params.constEnd(); ++it) {
1492 const QString &name = it.key();
1493 if (volatileParams.contains(name)) {
1494 continue;
1495 }
1496 const QVariant &value = it.value();
1497 const MAV_PARAM_TYPE mavType = _mapParamName2MavParamType[componentId][name];
1499
1500 crc = QGC::crc32(reinterpret_cast<const uint8_t *>(qPrintable(name)), name.length(), crc);
1501 crc = QGC::crc32(static_cast<const uint8_t *>(value.constData()), FactMetaData::typeToSize(factType), crc);
1502 }
1503 return crc;
1504}
1505
1506void MockLink::_handleParamRequestList(const mavlink_message_t &msg)
1507{
1509 return;
1510 }
1511
1512 mavlink_param_request_list_t request{};
1513 mavlink_msg_param_request_list_decode(&msg, &request);
1514
1515 Q_ASSERT(request.target_system == _vehicleSystemId);
1516 Q_ASSERT(request.target_component == MAV_COMP_ID_ALL);
1517
1518 // Cache component IDs and first component's param names to avoid repeated keys() calls in worker
1519 // Thread safety: Lock mutex before modifying shared state accessed by worker thread
1520 QMutexLocker locker(&_paramRequestListMutex);
1521 _paramRequestListComponentIds = _mapParamName2Value.keys();
1522 if (!_paramRequestListComponentIds.isEmpty()) {
1523 _paramRequestListParamNames = _mapParamName2Value[_paramRequestListComponentIds.first()].keys();
1524 }
1525
1526 // Start the worker routine
1527 _currentParamRequestListComponentIndex = 0;
1528 _currentParamRequestListParamIndex = 0;
1529 _paramRequestListHashCheckSent = false;
1530}
1531
1532void MockLink::_paramRequestListWorker()
1533{
1534 if (_currentParamRequestListComponentIndex == -1) {
1535 // Initial request complete
1536 return;
1537 }
1538
1539 // Thread safety: Lock mutex before accessing shared state modified by main thread
1540 QMutexLocker locker(&_paramRequestListMutex);
1541
1542 // Use cached lists instead of calling keys() on every iteration (500Hz)
1543 if (_currentParamRequestListComponentIndex >= _paramRequestListComponentIds.count()) {
1544 _currentParamRequestListComponentIndex = -1;
1545 return;
1546 }
1547
1548 const int componentId = _paramRequestListComponentIds.at(_currentParamRequestListComponentIndex);
1549 const int cParameters = _paramRequestListParamNames.count();
1550
1551 if (_currentParamRequestListParamIndex >= cParameters) {
1552 // All regular params sent — for PX4, append _HASH_CHECK as the last entry in the stream.
1553 // Uses param_count=0, param_index=-1 (same as standalone response) so ParameterManager
1554 // handles it via the _HASH_CHECK early-return path without affecting param count tracking.
1555 if (_firmwareType == MAV_AUTOPILOT_PX4 && !_paramRequestListHashCheckSent) {
1556 _paramRequestListHashCheckSent = true;
1557
1558 mavlink_param_union_t valueUnion{};
1559 valueUnion.type = MAV_PARAM_TYPE_UINT32;
1560 valueUnion.param_uint32 = _computeParamHash(componentId);
1561
1562 char paramId[MAVLINK_MSG_ID_PARAM_VALUE_LEN]{};
1563 (void) strncpy(paramId, "_HASH_CHECK", MAVLINK_MSG_ID_PARAM_VALUE_LEN);
1564
1565 qCDebug(MockLinkLog) << "Sending _HASH_CHECK in PARAM_REQUEST_LIST stream" << componentId << "hash:" << valueUnion.param_uint32;
1566
1567 mavlink_message_t responseMsg{};
1568 (void) mavlink_msg_param_value_pack_chan(
1569 _vehicleSystemId,
1570 componentId,
1571 _outgoingMavlinkChannel,
1572 &responseMsg,
1573 paramId,
1574 valueUnion.param_float,
1575 MAV_PARAM_TYPE_UINT32,
1576 0, // param_count: 0 to avoid affecting ParameterManager's count tracking
1577 -1 // param_index: -1 signals this is a virtual/out-of-band parameter
1578 );
1579 respondWithMavlinkMessage(responseMsg);
1580 return;
1581 }
1582
1583 // Move to next component
1584 if (++_currentParamRequestListComponentIndex >= _paramRequestListComponentIds.count()) {
1585 _currentParamRequestListComponentIndex = -1;
1586 _paramRequestListComponentIds.clear();
1587 _paramRequestListParamNames.clear();
1588 } else {
1589 // Cache param names for the new component
1590 _paramRequestListParamNames = _mapParamName2Value[_paramRequestListComponentIds.at(_currentParamRequestListComponentIndex)].keys();
1591 _currentParamRequestListParamIndex = 0;
1592 _paramRequestListHashCheckSent = false;
1593 }
1594 return;
1595 }
1596
1597 const QString &paramName = _paramRequestListParamNames.at(_currentParamRequestListParamIndex);
1598
1599 if (((_failureMode == MockConfiguration::FailMissingParamOnInitialRequest) || (_failureMode == MockConfiguration::FailMissingParamOnAllRequests)) && (paramName == _failParam)) {
1600 qCDebug(MockLinkLog) << "Skipping param send:" << paramName;
1601 } else {
1602 char paramId[MAVLINK_MSG_ID_PARAM_VALUE_LEN]{};
1603 mavlink_message_t responseMsg{};
1604
1605 Q_ASSERT(_mapParamName2Value[componentId].contains(paramName));
1606 Q_ASSERT(_mapParamName2MavParamType[componentId].contains(paramName));
1607
1608 const MAV_PARAM_TYPE paramType = _mapParamName2MavParamType[componentId][paramName];
1609
1610 Q_ASSERT(paramName.length() <= MAVLINK_MSG_ID_PARAM_VALUE_LEN);
1611 (void) strncpy(paramId, paramName.toLocal8Bit().constData(), MAVLINK_MSG_ID_PARAM_VALUE_LEN);
1612
1613 qCDebug(MockLinkLog) << "Sending msg_param_value" << componentId << paramId << paramType << _mapParamName2Value[componentId][paramId];
1614
1615 (void) mavlink_msg_param_value_pack_chan(
1616 _vehicleSystemId,
1617 componentId, // component id
1618 _outgoingMavlinkChannel,
1619 &responseMsg, // Outgoing message
1620 paramId, // Parameter name
1621 _floatUnionForParam(componentId, paramName), // Parameter value
1622 paramType, // MAV_PARAM_TYPE
1623 cParameters, // Total number of parameters
1624 _currentParamRequestListParamIndex // Index of this parameter
1625 );
1626 respondWithMavlinkMessage(responseMsg);
1627 }
1628
1629 // Move to next param index
1630 ++_currentParamRequestListParamIndex;
1631}
1632
1633void MockLink::_handleParamSet(const mavlink_message_t &msg)
1634{
1635 mavlink_param_set_t request{};
1636 mavlink_msg_param_set_decode(&msg, &request);
1637
1638 Q_ASSERT(request.target_system == _vehicleSystemId);
1639 const int componentId = request.target_component;
1640
1641 // Param may not be null terminated if exactly fits
1642 char paramId[MAVLINK_MSG_PARAM_SET_FIELD_PARAM_ID_LEN + 1]{};
1643 paramId[MAVLINK_MSG_PARAM_SET_FIELD_PARAM_ID_LEN] = 0;
1644 (void) strncpy(paramId, request.param_id, MAVLINK_MSG_PARAM_SET_FIELD_PARAM_ID_LEN);
1645
1646 qCDebug(MockLinkLog) << "_handleParamSet" << componentId << paramId << request.param_type;
1647
1648 // PX4 special case: _HASH_CHECK is a virtual parameter used by ParameterManager
1649 // to signal cache-hit and stop parameter streaming. It is intentionally not part
1650 // of the normal parameter maps.
1651 if ((_firmwareType == MAV_AUTOPILOT_PX4) && (strncmp(paramId, "_HASH_CHECK", MAVLINK_MSG_PARAM_SET_FIELD_PARAM_ID_LEN) == 0)) {
1652 QMutexLocker locker(&_paramRequestListMutex);
1653 _currentParamRequestListComponentIndex = -1;
1654 _paramRequestListComponentIds.clear();
1655 _paramRequestListParamNames.clear();
1656 qCDebug(MockLinkLog) << "Received _HASH_CHECK PARAM_SET, stopping parameter stream";
1657 return;
1658 }
1659
1660 // Real firmware rejects a PARAM_SET it doesn't recognize rather than crashing.
1661 // QGC can legitimately send these: loading a QGC-format param file sends params
1662 // not currently known to the vehicle (e.g. params unlocked by another param).
1663 if (!_mapParamName2Value.contains(componentId) || !_mapParamName2MavParamType.contains(componentId)) {
1664 qCDebug(MockLinkLog) << "_handleParamSet unknown component, rejecting with PARAM_ERROR - componentId:" << componentId << "param:" << paramId;
1665 _sendParamError(componentId, paramId, -1, MAV_PARAM_ERROR_COMPONENT_NOT_FOUND);
1666 return;
1667 }
1668 if (!_mapParamName2Value[componentId].contains(paramId)) {
1669 qCDebug(MockLinkLog) << "_handleParamSet unknown param, rejecting with PARAM_ERROR - componentId:" << componentId << "param:" << paramId;
1670 _sendParamError(componentId, paramId, -1, MAV_PARAM_ERROR_DOES_NOT_EXIST);
1671 return;
1672 }
1673 if (request.param_type != _mapParamName2MavParamType[componentId][paramId]) {
1674 qCDebug(MockLinkLog) << "_handleParamSet type mismatch, rejecting with PARAM_ERROR - param:" << paramId
1675 << "requested type:" << request.param_type << "actual type:" << _mapParamName2MavParamType[componentId][paramId];
1676 _sendParamError(componentId, paramId, -1, MAV_PARAM_ERROR_TYPE_MISMATCH);
1677 return;
1678 }
1679
1680 // Apply failure behaviors before committing change.
1681 if (_paramSetFailureMode == FailParamSetFirstAttemptNoAck && _paramSetFailureFirstAttemptPending) {
1682 qCDebug(MockLinkLog) << "Param set failure: first attempt no ack" << paramId;
1683 _paramSetFailureFirstAttemptPending = false;
1684 return;
1685 }
1686
1687 if (_paramSetFailureMode == FailParamSetNoAck) {
1688 qCDebug(MockLinkLog) << "Param set failure: no ack" << paramId;
1689 return;
1690 }
1691
1692 if (_paramSetFailureMode == FailParamSetParamError) {
1693 qCDebug(MockLinkLog) << "Param set failure: PARAM_ERROR" << paramId;
1694 _sendParamError(componentId, paramId,
1695 _mapParamName2Value[componentId].keys().indexOf(paramId),
1696 MAV_PARAM_ERROR_VALUE_OUT_OF_RANGE);
1697 return;
1698 }
1699
1700 // Normal success path
1701 _setParamFloatUnionIntoMap(componentId, paramId, request.param_value);
1702
1703 mavlink_message_t responseMsg;
1704 mavlink_msg_param_value_pack_chan(
1705 _vehicleSystemId,
1706 componentId, // component id
1707 _outgoingMavlinkChannel,
1708 &responseMsg, // Outgoing message
1709 paramId, // Parameter name
1710 request.param_value, // Send same value back
1711 request.param_type, // Send same type back
1712 _mapParamName2Value[componentId].count(), // Total number of parameters
1713 _mapParamName2Value[componentId].keys().indexOf(paramId) // Index of this parameter
1714 );
1715 respondWithMavlinkMessage(responseMsg);
1716}
1717
1718void MockLink::_handleParamRequestRead(const mavlink_message_t &msg)
1719{
1720 mavlink_message_t responseMsg{};
1721 mavlink_param_request_read_t request{};
1722 mavlink_msg_param_request_read_decode(&msg, &request);
1723
1724 const QString paramName(QString::fromLocal8Bit(request.param_id, static_cast<int>(strnlen(request.param_id, MAVLINK_MSG_PARAM_REQUEST_READ_FIELD_PARAM_ID_LEN))));
1725 const int componentId = request.target_component;
1726
1727 // special case for magic _HASH_CHECK value (PX4 only)
1728 if ((_firmwareType == MAV_AUTOPILOT_PX4) && (paramName == "_HASH_CHECK")) {
1729 _hashCheckRequestCount++;
1730 if (_hashCheckNoResponse) {
1731 return;
1732 }
1733
1734 const int hashComponentId = _mapParamName2Value.contains(MAV_COMP_ID_AUTOPILOT1)
1735 ? MAV_COMP_ID_AUTOPILOT1
1736 : _mapParamName2Value.keys().first();
1737
1738 mavlink_param_union_t valueUnion{};
1739 valueUnion.type = MAV_PARAM_TYPE_UINT32;
1740 valueUnion.param_uint32 = _computeParamHash(hashComponentId);
1741 (void) mavlink_msg_param_value_pack_chan(
1742 _vehicleSystemId,
1743 hashComponentId,
1744 _outgoingMavlinkChannel,
1745 &responseMsg,
1746 request.param_id,
1747 valueUnion.param_float,
1748 MAV_PARAM_TYPE_UINT32,
1749 0,
1750 -1
1751 );
1752 respondWithMavlinkMessage(responseMsg);
1753 return;
1754 }
1755
1756 if (!_mapParamName2Value.contains(componentId)) {
1757 qCDebug(MockLinkLog) << "_handleParamRequestRead unknown component, rejecting with PARAM_ERROR - componentId:" << componentId << "param:" << paramName;
1758 const QByteArray paramIdBytes = paramName.toLocal8Bit();
1759 _sendParamError(componentId, paramIdBytes.constData(), request.param_index, MAV_PARAM_ERROR_COMPONENT_NOT_FOUND);
1760 return;
1761 }
1762
1763 char paramId[MAVLINK_MSG_PARAM_REQUEST_READ_FIELD_PARAM_ID_LEN + 1]{};
1764 paramId[0] = 0;
1765
1766 Q_ASSERT(request.target_system == _vehicleSystemId);
1767
1768 if (request.param_index == -1) {
1769 // Request is by param name. Param may not be null terminated if exactly fits
1770 (void) strncpy(paramId, request.param_id, MAVLINK_MSG_PARAM_REQUEST_READ_FIELD_PARAM_ID_LEN);
1771 } else {
1772 // Request is by index
1773 Q_ASSERT(request.param_index >= 0 && request.param_index < _mapParamName2Value[componentId].count());
1774
1775 const QString key = _mapParamName2Value[componentId].keys().at(request.param_index);
1776 Q_ASSERT(key.length() <= MAVLINK_MSG_PARAM_REQUEST_READ_FIELD_PARAM_ID_LEN);
1777 strcpy(paramId, key.toLocal8Bit().constData());
1778 }
1779
1780 if (!_mapParamName2Value[componentId].contains(paramId) || !_mapParamName2MavParamType[componentId].contains(paramId)) {
1781 // Real firmware rejects a read of a parameter it doesn't know about (e.g. QGC
1782 // refreshing a param after a failed PARAM_SET for a param not on the vehicle)
1783 qCDebug(MockLinkLog) << "_handleParamRequestRead unknown param, rejecting with PARAM_ERROR - componentId:" << componentId << "param:" << paramId;
1784 _sendParamError(componentId, paramId, request.param_index, MAV_PARAM_ERROR_DOES_NOT_EXIST);
1785 return;
1786 }
1787
1788 if ((_failureMode == MockConfiguration::FailMissingParamOnAllRequests) && (strcmp(paramId, _failParam) == 0)) {
1789 qCDebug(MockLinkLog) << "Ignoring request read for " << _failParam;
1790 // Fail to send this param no matter what
1791 return;
1792 }
1793
1794 if (_paramRequestReadFailureMode == FailParamRequestReadFirstAttemptNoResponse && _paramRequestReadFailureFirstAttemptPending) {
1795 qCDebug(MockLinkLog) << "Param request read failure: first attempt no response" << paramId;
1796 _paramRequestReadFailureFirstAttemptPending = false;
1797 return;
1798 }
1799
1800 if (_paramRequestReadFailureMode == FailParamRequestReadNoResponse) {
1801 qCDebug(MockLinkLog) << "Param request read failure: no response" << paramId;
1802 return;
1803 }
1804
1805 if (_paramRequestReadFailureMode == FailParamRequestReadParamError) {
1806 qCDebug(MockLinkLog) << "Param request read failure: PARAM_ERROR" << paramId;
1807 _sendParamError(componentId, paramId, request.param_index, MAV_PARAM_ERROR_DOES_NOT_EXIST);
1808 return;
1809 }
1810
1811 (void) mavlink_msg_param_value_pack_chan(
1812 _vehicleSystemId,
1813 componentId, // component id
1814 _outgoingMavlinkChannel,
1815 &responseMsg, // Outgoing message
1816 paramId, // Parameter name
1817 _floatUnionForParam(componentId, paramId), // Parameter value
1818 _mapParamName2MavParamType[componentId][paramId], // Parameter type
1819 _mapParamName2Value[componentId].count(), // Total number of parameters
1820 _mapParamName2Value[componentId].keys().indexOf(paramId) // Index of this parameter
1821 );
1822 respondWithMavlinkMessage(responseMsg);
1823}
1824
1825void MockLink::_sendParamError(int componentId, const char *paramId, int16_t paramIndex, uint8_t errorCode)
1826{
1827 mavlink_message_t responseMsg{};
1828 char paramIdBuf[MAVLINK_MSG_PARAM_ERROR_FIELD_PARAM_ID_LEN + 1] = {};
1829 (void) strncpy(paramIdBuf, paramId, MAVLINK_MSG_PARAM_ERROR_FIELD_PARAM_ID_LEN);
1830
1831 (void) mavlink_msg_param_error_pack_chan(
1832 _vehicleSystemId,
1833 static_cast<uint8_t>(componentId),
1834 _outgoingMavlinkChannel,
1835 &responseMsg,
1838 paramIdBuf,
1839 paramIndex,
1840 errorCode
1841 );
1842 respondWithMavlinkMessage(responseMsg);
1843}
1844
1845void MockLink::_handleFTP(const mavlink_message_t &msg)
1846{
1847 _mockLinkFTP->mavlinkMessageReceived(msg);
1848}
1849
1850void MockLink::_handleInProgressCommandLong(const mavlink_command_long_t &request)
1851{
1852 uint8_t commandResult = MAV_RESULT_UNSUPPORTED;
1853
1854 switch (request.command) {
1856 // Test command which sends in progress messages and then acceptance ack
1857 commandResult = MAV_RESULT_ACCEPTED;
1858 break;
1860 // Test command which sends in progress messages and then failure ack
1861 commandResult = MAV_RESULT_FAILED;
1862 break;
1864 // Test command which sends in progress messages and then never sends final result ack
1865 break;
1866 }
1867
1868 mavlink_message_t commandAck{};
1869 (void) mavlink_msg_command_ack_pack_chan(
1870 _vehicleSystemId,
1871 _vehicleComponentId,
1872 _outgoingMavlinkChannel,
1873 &commandAck,
1874 request.command,
1875 MAV_RESULT_IN_PROGRESS,
1876 1, // progress
1877 0, // result_param2
1878 0, // target_system
1879 0 // target_component
1880 );
1881 respondWithMavlinkMessage(commandAck);
1882
1884 (void) mavlink_msg_command_ack_pack_chan(
1885 _vehicleSystemId,
1886 _vehicleComponentId,
1887 _outgoingMavlinkChannel,
1888 &commandAck,
1889 request.command,
1890 commandResult,
1891 0, // progress
1892 0, // result_param2
1893 0, // target_system
1894 0 // target_component
1895 );
1896 respondWithMavlinkMessage(commandAck);
1897 }
1898}
1899
1900void MockLink::_handleCommandLongSetMessageInterval(const mavlink_command_long_t &request, bool &accepted)
1901{
1902 // Accept only the message IDs that MAVLinkStreamConfig requests for PID tuning.
1903 // Anything else gets MAV_RESULT_UNSUPPORTED so unit tests will catch unexpected usage.
1904 static const QSet<int> kPidTuningMessageIds = {
1905 MAVLINK_MSG_ID_ATTITUDE_QUATERNION,
1906 MAVLINK_MSG_ID_ATTITUDE_TARGET,
1907 MAVLINK_MSG_ID_LOCAL_POSITION_NED,
1908 MAVLINK_MSG_ID_POSITION_TARGET_LOCAL_NED,
1909 MAVLINK_MSG_ID_NAV_CONTROLLER_OUTPUT,
1910 MAVLINK_MSG_ID_VFR_HUD,
1911 };
1912 accepted = kPidTuningMessageIds.contains(static_cast<int>(request.param1));
1913}
1914
1915void MockLink::_handleCommandLong(const mavlink_message_t &msg)
1916{
1917 static bool firstCmdUser3 = true;
1918 static bool firstCmdUser4 = true;
1919
1920 mavlink_command_long_t request{};
1921 mavlink_msg_command_long_decode(&msg, &request);
1922
1923 uint8_t commandResult = MAV_RESULT_UNSUPPORTED;
1924
1925 switch (request.command) {
1926 case MAV_CMD_COMPONENT_ARM_DISARM:
1927 if (request.param1 == 0.0f) {
1928 _mavBaseMode &= ~MAV_MODE_FLAG_SAFETY_ARMED;
1929 } else {
1930 _mavBaseMode |= MAV_MODE_FLAG_SAFETY_ARMED;
1931 }
1932 commandResult = MAV_RESULT_ACCEPTED;
1933 break;
1934 case MAV_CMD_PREFLIGHT_CALIBRATION:
1935 _handlePreFlightCalibration(request);
1936 commandResult = MAV_RESULT_ACCEPTED;
1937 break;
1938 case MAV_CMD_DO_MOTOR_TEST:
1939 commandResult = MAV_RESULT_ACCEPTED;
1940 break;
1941 case MAV_CMD_DO_START_MAG_CAL:
1942 // APM onboard compass calibration: start sending MAG_CAL_PROGRESS then MAG_CAL_REPORT.
1943 // Note: does NOT stop stale failed report streaming - like real ArduPilot, only
1944 // MAV_CMD_DO_CANCEL_MAG_CAL stops the failed report stream.
1945 if (_firmwareType == MAV_AUTOPILOT_ARDUPILOTMEGA) {
1946 if (_apmMagCalStartFailureMode) {
1947 commandResult = MAV_RESULT_FAILED;
1948 } else {
1949 QMutexLocker locker(&_apmCompassCalMutex);
1950 _apmCompassCalProgress = 0;
1951 _apmCompassCalTickCount = 0;
1952 commandResult = MAV_RESULT_ACCEPTED;
1953 }
1954 }
1955 break;
1956 case MAV_CMD_DO_CANCEL_MAG_CAL:
1957 // Stop APM compass calibration worker
1958 if (_firmwareType == MAV_AUTOPILOT_ARDUPILOTMEGA) {
1959 QMutexLocker locker(&_apmCompassCalMutex);
1960 _apmStaleFailedMagCalReportStreaming = false;
1961 _apmCompassCalProgress = -1;
1962 commandResult = MAV_RESULT_ACCEPTED;
1963 }
1964 break;
1965 case MAV_CMD_CONTROL_HIGH_LATENCY:
1966 if (linkConfiguration()->isHighLatency()) {
1967 _highLatencyTransmissionEnabled = static_cast<int>(request.param1) != 0;
1968 emit highLatencyTransmissionEnabledChanged(_highLatencyTransmissionEnabled);
1969 commandResult = MAV_RESULT_ACCEPTED;
1970 } else {
1971 commandResult = MAV_RESULT_FAILED;
1972 }
1973 break;
1974 case MAV_CMD_PREFLIGHT_STORAGE:
1975 if (static_cast<int>(request.param1) == 2) {
1976 // Reset all parameters to firmware defaults (unit test support)
1977 _resetParamsToDefaults();
1978 }
1979 commandResult = MAV_RESULT_ACCEPTED;
1980 break;
1981 case MAV_CMD_PREFLIGHT_REBOOT_SHUTDOWN:
1982 // Unit test support: accept the reboot command so tests can exercise
1983 // flows which restart the vehicle (e.g. airframe Apply and Restart).
1984 // The actual reboot is not simulated.
1985 commandResult = MAV_RESULT_ACCEPTED;
1986 break;
1987 case MAV_CMD_REQUEST_AUTOPILOT_CAPABILITIES:
1988 commandResult = MAV_RESULT_ACCEPTED;
1989 _respondWithAutopilotVersion();
1990 break;
1991 case MAV_CMD_REQUEST_MESSAGE:
1992 {
1993 bool accepted = false;
1994 bool noAck = false;
1995 _handleRequestMessage(request, accepted, noAck);
1996 if (noAck) {
1997 // FailRequestMessageCommandNoResponse: don't send any ack, let vehicle timeout
1998 return;
1999 }
2000 if (accepted) {
2001 commandResult = MAV_RESULT_ACCEPTED;
2002 }
2003 break;
2004 }
2005 case MAV_CMD_NAV_TAKEOFF:
2006 _handleTakeoff(request);
2007 commandResult = MAV_RESULT_ACCEPTED;
2008 break;
2010 // Test command which always returns MAV_RESULT_ACCEPTED
2011 commandResult = MAV_RESULT_ACCEPTED;
2012 break;
2014 // Test command which always returns MAV_RESULT_FAILED
2015 commandResult = MAV_RESULT_FAILED;
2016 break;
2018 // Test command which does not respond to first request and returns MAV_RESULT_ACCEPTED on second attempt
2019 if (firstCmdUser3) {
2020 firstCmdUser3 = false;
2021 return;
2022 } else {
2023 firstCmdUser3 = true;
2024 commandResult = MAV_RESULT_ACCEPTED;
2025 }
2026 break;
2028 // Test command which does not respond to first request and returns MAV_RESULT_FAILED on second attempt
2029 if (firstCmdUser4) {
2030 firstCmdUser4 = false;
2031 return;
2032 } else {
2033 firstCmdUser4 = true;
2034 commandResult = MAV_RESULT_FAILED;
2035 }
2036 break;
2039 // Test command which never responds
2040 return;
2044 _handleInProgressCommandLong(request);
2045 return;
2046 case MAV_CMD_SET_MESSAGE_INTERVAL:
2047 {
2048 bool accepted = false;
2049
2050 _handleCommandLongSetMessageInterval(request, accepted);
2051 if (accepted) {
2052 commandResult = MAV_RESULT_ACCEPTED;
2053 }
2054 break;
2055 }
2056 }
2057
2058 mavlink_message_t commandAck{};
2059 (void) mavlink_msg_command_ack_pack_chan(
2060 _vehicleSystemId,
2061 _vehicleComponentId,
2062 _outgoingMavlinkChannel,
2063 &commandAck,
2064 request.command,
2065 commandResult,
2066 0, // progress
2067 0, // result_param2
2068 0, // target_system
2069 0 // target_component
2070 );
2071 respondWithMavlinkMessage(commandAck);
2072}
2073
2074void MockLink::_handleCommandInt(const mavlink_message_t &msg)
2075{
2076 mavlink_command_int_t request{};
2077 mavlink_msg_command_int_decode(&msg, &request);
2078
2079 // MockLink does not implement any COMMAND_INT commands yet, so it reports them as
2080 // unsupported (mirroring the COMMAND_LONG default for unrecognized commands). This
2081 // lets unit tests exercise "try command, fall back to legacy message" code paths
2082 // such as Vehicle::setEstimatorOrigin.
2083 const uint8_t commandResult = MAV_RESULT_UNSUPPORTED;
2084
2085 mavlink_message_t commandAck{};
2086 (void) mavlink_msg_command_ack_pack_chan(
2087 _vehicleSystemId,
2088 _vehicleComponentId,
2089 _outgoingMavlinkChannel,
2090 &commandAck,
2091 request.command,
2092 commandResult,
2093 0, // progress
2094 0, // result_param2
2095 0, // target_system
2096 0 // target_component
2097 );
2098 respondWithMavlinkMessage(commandAck);
2099}
2100
2101void MockLink::sendUnexpectedCommandAck(MAV_CMD command, MAV_RESULT ackResult)
2102{
2103 mavlink_message_t commandAck{};
2104 (void) mavlink_msg_command_ack_pack_chan(
2105 _vehicleSystemId,
2106 _vehicleComponentId,
2107 _outgoingMavlinkChannel,
2108 &commandAck,
2109 command,
2110 ackResult,
2111 0, // progress
2112 0, // result_param2
2113 0, // target_system
2114 0 // target_component
2115 );
2116 respondWithMavlinkMessage(commandAck);
2117}
2118
2119void MockLink::_respondWithAutopilotVersion()
2120{
2121 union FlightVersion {
2122 uint32_t raw;
2123
2124 struct {
2125 uint8_t type; // bits 0–7
2126 uint8_t patch; // bits 8–15
2127 uint8_t minor; // bits 16–23
2128 uint8_t major; // bits 24–31
2129 } parts;
2130
2131 FlightVersion(uint32_t version = 0) : raw(version) {}
2132 };
2133 FlightVersion flightVersion;
2134
2135 if (_firmwareType == MAV_AUTOPILOT_ARDUPILOTMEGA) {
2136 flightVersion.parts.major = 4;
2137 flightVersion.parts.minor = 7;
2138 flightVersion.parts.patch = 0;
2139 flightVersion.parts.type = FIRMWARE_VERSION_TYPE_OFFICIAL;
2140 } else if (_firmwareType == MAV_AUTOPILOT_PX4) {
2141 flightVersion.parts.major = 1;
2142 flightVersion.parts.minor = 17;
2143 flightVersion.parts.patch = 0;
2144 flightVersion.parts.type = FIRMWARE_VERSION_TYPE_OFFICIAL;
2145 }
2146
2147 const uint8_t customVersion[8]{};
2148 const uint64_t capabilities = MAV_PROTOCOL_CAPABILITY_MAVLINK2 | MAV_PROTOCOL_CAPABILITY_MISSION_FENCE | MAV_PROTOCOL_CAPABILITY_MISSION_RALLY | MAV_PROTOCOL_CAPABILITY_MISSION_INT
2149 | ((_firmwareType == MAV_AUTOPILOT_ARDUPILOTMEGA) ? MAV_PROTOCOL_CAPABILITY_TERRAIN : 0)
2150 | (_ftpCapability ? MAV_PROTOCOL_CAPABILITY_FTP : 0);
2151
2152 mavlink_message_t msg{};
2153 (void) mavlink_msg_autopilot_version_pack_chan(
2154 _vehicleSystemId,
2155 _vehicleComponentId,
2156 _outgoingMavlinkChannel,
2157 &msg,
2158 capabilities,
2159 flightVersion.raw, // flight_sw_version,
2160 0, // middleware_sw_version,
2161 0, // os_sw_version,
2162 0, // board_version,
2163 reinterpret_cast<const uint8_t*>(&customVersion), // flight_custom_version,
2164 reinterpret_cast<const uint8_t*>(&customVersion), // middleware_custom_version,
2165 reinterpret_cast<const uint8_t*>(&customVersion), // os_custom_version,
2166 _boardVendorId,
2167 _boardProductId,
2168 0, // uid
2169 0 // uid2
2170 );
2172}
2173
2174void MockLink::_sendHomePosition()
2175{
2176 const float bogus[4]{};
2177
2178 mavlink_message_t msg{};
2179 (void) mavlink_msg_home_position_pack_chan(
2180 _vehicleSystemId,
2181 _vehicleComponentId,
2182 _outgoingMavlinkChannel,
2183 &msg,
2184 static_cast<int32_t>(_vehicleLatitude * 1E7),
2185 static_cast<int32_t>(_vehicleLongitude * 1E7),
2186 static_cast<int32_t>(_defaultVehicleHomeAltitude * 1000),
2187 0.0f, 0.0f, 0.0f,
2188 &bogus[0],
2189 0.0f, 0.0f, 0.0f,
2190 0
2191 );
2193}
2194
2195void MockLink::_sendGpsRawInt()
2196{
2197 static uint64_t timeTick = 0;
2198
2199 mavlink_message_t msg{};
2200 (void) mavlink_msg_gps_raw_int_pack_chan(
2201 _vehicleSystemId,
2202 _vehicleComponentId,
2203 _outgoingMavlinkChannel,
2204 &msg,
2205 timeTick++, // time since boot
2206 GPS_FIX_TYPE_3D_FIX,
2207 static_cast<int32_t>(_vehicleLatitude * 1E7),
2208 static_cast<int32_t>(_vehicleLongitude * 1E7),
2209 static_cast<int32_t>(_vehicleAltitudeAMSL * 1000),
2210 3 * 100, // hdop
2211 3 * 100, // vdop
2212 UINT16_MAX, // velocity not known
2213 UINT16_MAX, // course over ground not known
2214 8, // satellites visible
2215 //-- Extension
2216 0, // Altitude (above WGS84, EGM96 ellipsoid), in meters * 1000 (positive for up).
2217 0, // Position uncertainty in meters * 1000 (positive for up).
2218 0, // Altitude uncertainty in meters * 1000 (positive for up).
2219 0, // Speed uncertainty in meters * 1000 (positive for up).
2220 0, // Heading / track uncertainty in degrees * 1e5.
2221 65535 // Yaw not provided
2222 );
2224}
2225
2226void MockLink::_sendGlobalPositionInt()
2227{
2228 static uint64_t timeTick = 0;
2229
2230 mavlink_message_t msg{};
2231 (void) mavlink_msg_global_position_int_pack_chan(
2232 _vehicleSystemId,
2233 _vehicleComponentId,
2234 _outgoingMavlinkChannel,
2235 &msg,
2236 timeTick++, // time since boot
2237 static_cast<int32_t>(_vehicleLatitude * 1E7),
2238 static_cast<int32_t>(_vehicleLongitude * 1E7),
2239 static_cast<int32_t>(_vehicleAltitudeAMSL * 1000),
2240 static_cast<int32_t>((_vehicleAltitudeAMSL - _defaultVehicleHomeAltitude) * 1000),
2241 0, 0, 0, // no speed sent
2242 UINT16_MAX // no heading sent
2243 );
2245}
2246
2247void MockLink::_sendAttitudeQuaternion()
2248{
2249 const uint32_t timeBootMs = static_cast<uint32_t>(_runningTime.elapsed());
2250 const float t = timeBootMs / 1000.0f;
2251
2252 // Synthesize sinusoidal Euler angles (rad)
2253 const float roll = 0.20f * std::sin(2.0f * static_cast<float>(M_PI) * 0.50f * t);
2254 const float pitch = 0.10f * std::sin(2.0f * static_cast<float>(M_PI) * 0.40f * t);
2255 const float yaw = 0.30f * std::sin(2.0f * static_cast<float>(M_PI) * 0.10f * t);
2256
2257 // ZYX Euler → quaternion
2258 const float cr = std::cos(roll / 2.0f), sr = std::sin(roll / 2.0f);
2259 const float cp = std::cos(pitch / 2.0f), sp = std::sin(pitch / 2.0f);
2260 const float cy = std::cos(yaw / 2.0f), sy = std::sin(yaw / 2.0f);
2261 const float q1 = cr * cp * cy + sr * sp * sy; // w
2262 const float q2 = sr * cp * cy - cr * sp * sy; // x
2263 const float q3 = cr * sp * cy + sr * cp * sy; // y
2264 const float q4 = cr * cp * sy - sr * sp * cy; // z
2265
2266 // Body rates = time-derivatives of the Euler angles
2267 const float rollspeed = 0.20f * (2.0f * static_cast<float>(M_PI) * 0.50f) * std::cos(2.0f * static_cast<float>(M_PI) * 0.50f * t);
2268 const float pitchspeed = 0.10f * (2.0f * static_cast<float>(M_PI) * 0.40f) * std::cos(2.0f * static_cast<float>(M_PI) * 0.40f * t);
2269 const float yawspeed = 0.30f * (2.0f * static_cast<float>(M_PI) * 0.10f) * std::cos(2.0f * static_cast<float>(M_PI) * 0.10f * t);
2270
2271 const float reprOffsetQ[4] = {1.0f, 0.0f, 0.0f, 0.0f}; // identity
2272
2273 mavlink_message_t msg{};
2274 (void) mavlink_msg_attitude_quaternion_pack_chan(
2275 _vehicleSystemId,
2276 _vehicleComponentId,
2277 _outgoingMavlinkChannel,
2278 &msg,
2279 timeBootMs,
2280 q1, q2, q3, q4,
2281 rollspeed, pitchspeed, yawspeed,
2282 reprOffsetQ
2283 );
2285}
2286
2287void MockLink::_sendAttitudeTarget()
2288{
2289 // Setpoint: same shape as actual, phase-shifted by +0.3 rad
2290 const uint32_t timeBootMs = static_cast<uint32_t>(_runningTime.elapsed());
2291 const float t = timeBootMs / 1000.0f;
2292 static constexpr float kPhase = 0.3f; // rad
2293
2294 const float roll = 0.20f * std::sin(2.0f * static_cast<float>(M_PI) * 0.50f * t + kPhase);
2295 const float pitch = 0.10f * std::sin(2.0f * static_cast<float>(M_PI) * 0.40f * t + kPhase);
2296 const float yaw = 0.30f * std::sin(2.0f * static_cast<float>(M_PI) * 0.10f * t + kPhase);
2297
2298 const float cr = std::cos(roll / 2.0f), sr = std::sin(roll / 2.0f);
2299 const float cp = std::cos(pitch / 2.0f), sp = std::sin(pitch / 2.0f);
2300 const float cy = std::cos(yaw / 2.0f), sy = std::sin(yaw / 2.0f);
2301 const float qSp[4] = {
2302 cr * cp * cy + sr * sp * sy,
2303 sr * cp * cy - cr * sp * sy,
2304 cr * sp * cy + sr * cp * sy,
2305 cr * cp * sy - sr * sp * cy,
2306 };
2307
2308 const float bodyRollRate = 0.20f * (2.0f * static_cast<float>(M_PI) * 0.50f) * std::cos(2.0f * static_cast<float>(M_PI) * 0.50f * t + kPhase);
2309 const float bodyPitchRate = 0.10f * (2.0f * static_cast<float>(M_PI) * 0.40f) * std::cos(2.0f * static_cast<float>(M_PI) * 0.40f * t + kPhase);
2310 const float bodyYawRate = 0.30f * (2.0f * static_cast<float>(M_PI) * 0.10f) * std::cos(2.0f * static_cast<float>(M_PI) * 0.10f * t + kPhase);
2311
2312 mavlink_message_t msg{};
2313 (void) mavlink_msg_attitude_target_pack_chan(
2314 _vehicleSystemId,
2315 _vehicleComponentId,
2316 _outgoingMavlinkChannel,
2317 &msg,
2318 timeBootMs,
2319 0, // type_mask: all fields valid
2320 qSp,
2321 bodyRollRate, bodyPitchRate, bodyYawRate,
2322 0.5f // thrust
2323 );
2325}
2326
2327void MockLink::_sendLocalPositionNed()
2328{
2329 const uint32_t timeBootMs = static_cast<uint32_t>(_runningTime.elapsed());
2330 const float t = timeBootMs / 1000.0f;
2331
2332 const float x = 5.0f * std::sin(2.0f * static_cast<float>(M_PI) * 0.08f * t);
2333 const float y = 5.0f * std::sin(2.0f * static_cast<float>(M_PI) * 0.10f * t);
2334 const float z = -10.0f + 1.0f * std::sin(2.0f * static_cast<float>(M_PI) * 0.15f * t); // negative = up in NED
2335 const float vx = 5.0f * (2.0f * static_cast<float>(M_PI) * 0.08f) * std::cos(2.0f * static_cast<float>(M_PI) * 0.08f * t);
2336 const float vy = 5.0f * (2.0f * static_cast<float>(M_PI) * 0.10f) * std::cos(2.0f * static_cast<float>(M_PI) * 0.10f * t);
2337 const float vz = 1.0f * (2.0f * static_cast<float>(M_PI) * 0.15f) * std::cos(2.0f * static_cast<float>(M_PI) * 0.15f * t);
2338
2339 mavlink_message_t msg{};
2340 (void) mavlink_msg_local_position_ned_pack_chan(
2341 _vehicleSystemId,
2342 _vehicleComponentId,
2343 _outgoingMavlinkChannel,
2344 &msg,
2345 timeBootMs,
2346 x, y, z,
2347 vx, vy, vz
2348 );
2350}
2351
2352void MockLink::_sendPositionTargetLocalNed()
2353{
2354 // Setpoint: same shape as _sendLocalPositionNed, with a 0.5 s time offset (not a phase in rad)
2355 const uint32_t timeBootMs = static_cast<uint32_t>(_runningTime.elapsed());
2356 const float t = timeBootMs / 1000.0f + 0.5f; // +0.5 s time lead
2357
2358 const float x = 5.0f * std::sin(2.0f * static_cast<float>(M_PI) * 0.08f * t);
2359 const float y = 5.0f * std::sin(2.0f * static_cast<float>(M_PI) * 0.10f * t);
2360 const float z = -10.0f + 1.0f * std::sin(2.0f * static_cast<float>(M_PI) * 0.15f * t);
2361 const float vx = 5.0f * (2.0f * static_cast<float>(M_PI) * 0.08f) * std::cos(2.0f * static_cast<float>(M_PI) * 0.08f * t);
2362 const float vy = 5.0f * (2.0f * static_cast<float>(M_PI) * 0.10f) * std::cos(2.0f * static_cast<float>(M_PI) * 0.10f * t);
2363 const float vz = 1.0f * (2.0f * static_cast<float>(M_PI) * 0.15f) * std::cos(2.0f * static_cast<float>(M_PI) * 0.15f * t);
2364
2365 mavlink_message_t msg{};
2366 (void) mavlink_msg_position_target_local_ned_pack_chan(
2367 _vehicleSystemId,
2368 _vehicleComponentId,
2369 _outgoingMavlinkChannel,
2370 &msg,
2371 timeBootMs,
2372 MAV_FRAME_LOCAL_NED,
2373 0, // type_mask: all fields valid
2374 x, y, z,
2375 vx, vy, vz,
2376 0.0f, 0.0f, 0.0f, // acceleration not used
2377 0.0f, 0.0f // yaw, yaw_rate not used
2378 );
2380}
2381
2382void MockLink::_sendExtendedSysState()
2383{
2384 mavlink_message_t msg{};
2385 (void) mavlink_msg_extended_sys_state_pack_chan(
2386 _vehicleSystemId,
2387 _vehicleComponentId,
2388 _outgoingMavlinkChannel,
2389 &msg,
2390 MAV_VTOL_STATE_UNDEFINED,
2391 (_vehicleAltitudeAMSL > _defaultVehicleHomeAltitude) ? MAV_LANDED_STATE_IN_AIR : MAV_LANDED_STATE_ON_GROUND
2392 );
2394}
2395
2396void MockLink::_sendChunkedStatusText(uint16_t chunkId, bool missingChunks)
2397{
2398 constexpr int cChunks = 4;
2399
2400 int num = 0;
2401 for (int i = 0; i < cChunks; i++) {
2402 if (missingChunks && (i & 1)) {
2403 continue;
2404 }
2405
2406 int cBuf = MAVLINK_MSG_STATUSTEXT_FIELD_TEXT_LEN;
2407 char msgBuf[MAVLINK_MSG_STATUSTEXT_FIELD_TEXT_LEN]{};
2408
2409 if (i == cChunks - 1) {
2410 // Last chunk is partial
2411 cBuf /= 2;
2412 }
2413
2414 for (int j = 0; j < cBuf - 1; j++) {
2415 msgBuf[j] = '0' + num++;
2416 if (num > 9) {
2417 num = 0;
2418 }
2419 }
2420 msgBuf[cBuf-1] = 'A' + i;
2421
2422 mavlink_message_t msg{};
2423 (void) mavlink_msg_statustext_pack_chan(
2424 _vehicleSystemId,
2425 _vehicleComponentId,
2426 _outgoingMavlinkChannel,
2427 &msg,
2428 MAV_SEVERITY_INFO,
2429 msgBuf,
2430 chunkId,
2431 i // chunk sequence number
2432 );
2434 }
2435}
2436
2437void MockLink::_sendStatusTextMessages()
2438{
2439 struct StatusMessage {
2440 MAV_SEVERITY severity;
2441 const char *msg;
2442 };
2443
2444 static constexpr struct StatusMessage rgMessages[] = {
2445 { MAV_SEVERITY_INFO, "#Testing audio output" },
2446 { MAV_SEVERITY_EMERGENCY, "Status text emergency" },
2447 { MAV_SEVERITY_ALERT, "Status text alert" },
2448 { MAV_SEVERITY_CRITICAL, "Status text critical" },
2449 { MAV_SEVERITY_ERROR, "Status text error" },
2450 { MAV_SEVERITY_WARNING, "Status text warning" },
2451 { MAV_SEVERITY_NOTICE, "Status text notice" },
2452 { MAV_SEVERITY_INFO, "Status text info" },
2453 { MAV_SEVERITY_DEBUG, "Status text debug" },
2454 };
2455
2456 mavlink_message_t msg{};
2457 for (size_t i = 0; i < std::size(rgMessages); i++) {
2458 const struct StatusMessage *status = &rgMessages[i];
2459 char statusText[MAVLINK_MSG_STATUSTEXT_FIELD_TEXT_LEN] = {};
2460 (void) std::strncpy(statusText, status->msg, sizeof(statusText) - 1);
2461
2462 (void) mavlink_msg_statustext_pack_chan(
2463 _vehicleSystemId,
2464 _vehicleComponentId,
2465 _outgoingMavlinkChannel,
2466 &msg,
2467 status->severity,
2468 statusText,
2469 0, // Not a chunked sequence
2470 0 // Not a chunked sequence
2471 );
2473 }
2474
2475 _sendChunkedStatusText(1, false /* missingChunks */);
2476 _sendChunkedStatusText(2, true /* missingChunks */);
2477 _sendChunkedStatusText(3, false /* missingChunks */); // This should cause the previous incomplete chunk to spit out
2478 _sendChunkedStatusText(4, true /* missingChunks */); // This should cause the timeout to fire
2479}
2480
2481MockLink *MockLink::_startMockLink(MockConfiguration *mockConfig)
2482{
2483 mockConfig->setDynamic(true);
2485
2486 if (LinkManager::instance()->createConnectedLink(config)) {
2487 return qobject_cast<MockLink*>(config->link());
2488 }
2489
2490 return nullptr;
2491}
2492
2493MockLink *MockLink::_startMockLinkWorker(const QString &configName, MAV_AUTOPILOT firmwareType, MAV_TYPE vehicleType, MockConfiguration::Options options, MockConfiguration::FailureMode_t failureMode, MockConfiguration::VideoStreamType videoStreamType)
2494{
2495 MockConfiguration *const mockConfig = new MockConfiguration(configName);
2496
2497 mockConfig->setFirmwareType(firmwareType);
2498 mockConfig->setVehicleType(vehicleType);
2499 mockConfig->setSendStatusText(options.testFlag(MockConfiguration::OptionSendStatusText));
2500 mockConfig->setEnableCamera(options.testFlag(MockConfiguration::OptionEnableCamera));
2501 mockConfig->setEnableGimbal(options.testFlag(MockConfiguration::OptionEnableGimbal));
2502 mockConfig->setEnableProximity(options.testFlag(MockConfiguration::OptionEnableProximity));
2503 mockConfig->setPreloadMission(options.testFlag(MockConfiguration::OptionPreloadMission));
2504 mockConfig->setStayMavlinkV1(options.testFlag(MockConfiguration::OptionStayMavlinkV1));
2506 mockConfig->setFtpCapability(options.testFlag(MockConfiguration::OptionFtpCapability));
2507 mockConfig->setVideoStreamType(videoStreamType);
2508 mockConfig->setFailureMode(failureMode);
2509
2510 return _startMockLink(mockConfig);
2511}
2512
2513MockLink *MockLink::startPX4MockLink(MockConfiguration::Options options, MockConfiguration::FailureMode_t failureMode, MockConfiguration::VideoStreamType videoStreamType)
2514{
2515 return _startMockLinkWorker(QStringLiteral("PX4 MultiRotor MockLink"), MAV_AUTOPILOT_PX4, MAV_TYPE_QUADROTOR, options, failureMode, videoStreamType);
2516}
2517
2519{
2520 return _startMockLinkWorker(QStringLiteral("PX4 MultiRotor MockLink"), MAV_AUTOPILOT_PX4, MAV_TYPE_QUADROTOR, options | MockConfiguration::OptionPreloadMission, failureMode);
2521}
2522
2524{
2525 return _startMockLinkWorker(QStringLiteral("Generic MockLink"), MAV_AUTOPILOT_GENERIC, MAV_TYPE_QUADROTOR, options, failureMode, videoStreamType);
2526}
2527
2529{
2530 return _startMockLinkWorker(QStringLiteral("No Initial Connect MockLink"), MAV_AUTOPILOT_PX4, MAV_TYPE_GENERIC, options, failureMode);
2531}
2532
2534{
2535 return _startMockLinkWorker(QStringLiteral("ArduCopter MockLink"),MAV_AUTOPILOT_ARDUPILOTMEGA, MAV_TYPE_QUADROTOR, options, failureMode, videoStreamType);
2536}
2537
2539{
2540 return _startMockLinkWorker(QStringLiteral("ArduPlane MockLink"), MAV_AUTOPILOT_ARDUPILOTMEGA, MAV_TYPE_FIXED_WING, options, failureMode, videoStreamType);
2541}
2542
2544{
2545 return _startMockLinkWorker(QStringLiteral("ArduSub MockLink"), MAV_AUTOPILOT_ARDUPILOTMEGA, MAV_TYPE_SUBMARINE, options, failureMode, videoStreamType);
2546}
2547
2549{
2550 return _startMockLinkWorker(QStringLiteral("ArduRover MockLink"), MAV_AUTOPILOT_ARDUPILOTMEGA, MAV_TYPE_GROUND_ROVER, options, failureMode, videoStreamType);
2551}
2552
2553void MockLink::_sendRCChannels()
2554{
2555 mavlink_message_t msg{};
2556 (void) mavlink_msg_rc_channels_pack_chan(
2557 _vehicleSystemId,
2558 _vehicleComponentId,
2559 _outgoingMavlinkChannel,
2560 &msg,
2561 0, // time_boot_ms
2562 16, // chancount
2563 1500, 1500, 1500, 1500, 1500, 1500, 1500, 1500, // channel 1-8
2564 1500, 1500, 1500, 1500, 1500, 1500, 1500, 1500, // channel 9-16
2565 UINT16_MAX, UINT16_MAX, // channel 17/18 unused
2566 0 // rssi
2567 );
2569}
2570
2571void MockLink::_handlePreFlightCalibration(const mavlink_command_long_t& request)
2572{
2573 if ((request.param1 == 0) && (request.param2 == 0) && (request.param3 == 0) &&
2574 (request.param4 == 0) && (request.param5 == 0) && (request.param6 == 0) &&
2575 (request.param7 == 0)) {
2576 // All zeros is a calibration cancel request.
2577 if (_firmwareType == MAV_AUTOPILOT_ARDUPILOTMEGA) {
2578 // Send ACCELCAL_VEHICLE_POS_FAILED so controller calls _stopCalibration(Failed)
2579 QMutexLocker locker(&_apmAccelCalMutex);
2580 if (_apmAccelCalPosIndex >= 0) {
2581 mavlink_message_t msg{};
2583 cmd.target_system = 255;
2584 cmd.target_component = MAV_COMP_ID_MISSIONPLANNER;
2585 cmd.command = MAV_CMD_ACCELCAL_VEHICLE_POS;
2586 cmd.param1 = static_cast<float>(ACCELCAL_VEHICLE_POS_FAILED);
2587 (void) mavlink_msg_command_long_encode_chan(
2588 _vehicleSystemId, _vehicleComponentId, _outgoingMavlinkChannel, &msg, &cmd);
2590 _apmAccelCalPosIndex = -1;
2591 }
2592 } else {
2593 // PX4: See PX4 calibrate_cancel_check().
2594 (void) _mockLinkPX4Calibration->cancel();
2595 }
2596 return;
2597 }
2598
2599 if (request.param2 == 1) {
2600 // Magnetometer calibration runs the full pose-driven simulation
2601 _mockLinkPX4Calibration->startMagCalibration();
2602 return;
2603 }
2604
2605 if (request.param1 == 1) {
2606 sendStatusTextMessage(MAV_SEVERITY_INFO, QStringLiteral("[cal] calibration started: 2 gyro"));
2607 return;
2608 }
2609
2610 if (request.param5 == 1) {
2611 if (_firmwareType == MAV_AUTOPILOT_ARDUPILOTMEGA) {
2612 // APM full accelerometer calibration: drive the ACCELCAL_VEHICLE_POS handshake
2613 QMutexLocker locker(&_apmAccelCalMutex);
2614 _apmAccelCalPosIndex = 0;
2615 _apmAccelCalGotAck = false;
2616 _apmAccelCalTickCount = 0;
2617 } else {
2618 // Accelerometer calibration runs the full pose-driven simulation
2619 _mockLinkPX4Calibration->startAccelCalibration();
2620 }
2621 }
2622}
2623
2624void MockLink::sendStatusTextMessage(uint8_t severity, const QString &text)
2625{
2626 char statusText[MAVLINK_MSG_STATUSTEXT_FIELD_TEXT_LEN] = {};
2627 (void) std::strncpy(statusText, text.toUtf8().constData(), sizeof(statusText) - 1);
2628
2629 mavlink_message_t msg{};
2630 (void) mavlink_msg_statustext_pack_chan(
2631 _vehicleSystemId,
2632 _vehicleComponentId,
2633 _outgoingMavlinkChannel,
2634 &msg,
2635 severity,
2636 statusText,
2637 0,
2638 0 // Not chunked
2639 );
2641}
2642
2643void MockLink::_handleTakeoff(const mavlink_command_long_t &request)
2644{
2645 _vehicleAltitudeAMSL = request.param7 + _defaultVehicleHomeAltitude;
2646 _mavBaseMode |= MAV_MODE_FLAG_SAFETY_ARMED;
2647}
2648
2649void MockLink::_handleLogRequestList(const mavlink_message_t &msg)
2650{
2651 mavlink_log_request_list_t request{};
2652 mavlink_msg_log_request_list_decode(&msg, &request);
2653
2654 if ((request.start != 0) && (request.end != 0xffff)) {
2655 qCWarning(MockLinkLog) << "_handleLogRequestList cannot handle partial requests";
2656 return;
2657 }
2658
2659 // When simulated FTP log files are set, LOG_ENTRY responses describe the same logs so
2660 // both transports report a consistent log list (matching PX4 behavior).
2661 const QList<MockLinkFTP::LogFile> logFiles = _mockLinkFTP->logFiles();
2662 if (!_logsErased && !logFiles.isEmpty()) {
2663 const uint16_t numLogs = static_cast<uint16_t>(logFiles.count());
2664 for (uint16_t id = 0; id < numLogs; id++) {
2665 mavlink_message_t responseMsg{};
2666 (void) mavlink_msg_log_entry_pack_chan(
2667 _vehicleSystemId,
2668 _vehicleComponentId,
2669 _outgoingMavlinkChannel,
2670 &responseMsg,
2671 id, // log id
2672 numLogs, // num_logs
2673 numLogs - 1, // last_log_num
2674 logFiles[id].mtime, // time_utc
2675 static_cast<uint32_t>(logFiles[id].size) // size
2676 );
2677 respondWithMavlinkMessage(responseMsg);
2678 }
2679 return;
2680 }
2681
2682 const uint16_t numLogs = _logsErased ? 0 : 1;
2683 const uint16_t logId = _logsErased ? 0 : _logDownloadLogId;
2684 const uint32_t logSize = _logsErased ? 0 : _logDownloadFileSize;
2685 mavlink_message_t responseMsg{};
2686 mavlink_msg_log_entry_pack_chan(
2687 _vehicleSystemId,
2688 _vehicleComponentId,
2689 _outgoingMavlinkChannel,
2690 &responseMsg,
2691 logId, // log id
2692 numLogs, // num_logs
2693 numLogs, // last_log_num
2694 0, // time_utc
2695 logSize // size
2696 );
2697 respondWithMavlinkMessage(responseMsg);
2698}
2699
2700void MockLink::_handleLogErase(const mavlink_message_t &msg)
2701{
2702 mavlink_log_erase_t request{};
2703 mavlink_msg_log_erase_decode(&msg, &request);
2704
2705 if ((request.target_system != _vehicleSystemId) || (request.target_component != _vehicleComponentId)) {
2706 return;
2707 }
2708
2709 _logsErased = true;
2710 _mockLinkFTP->setLogFiles({});
2711}
2712
2713QString MockLink::_createRandomFile(uint32_t byteCount)
2714{
2715 QTemporaryFile tempFile;
2716 tempFile.setAutoRemove(false);
2717 if (!tempFile.open()) {
2718 qCWarning(MockLinkLog) << "MockLink::createRandomFile open failed" << tempFile.errorString();
2719 return QString();
2720 }
2721
2722 for (uint32_t bytesWritten = 0; bytesWritten < byteCount; bytesWritten++) {
2723 const unsigned char byte = (QRandomGenerator::global()->generate() * 0xFF) / RAND_MAX;
2724 (void) tempFile.write(reinterpret_cast<const char*>(&byte), 1);
2725 }
2726
2727 tempFile.close();
2728 return tempFile.fileName();
2729}
2730
2731QString MockLink::_createLogContentsFile(const QString &logName)
2732{
2733 QTemporaryFile tempFile;
2734 tempFile.setAutoRemove(false);
2735 if (!tempFile.open()) {
2736 qCWarning(MockLinkLog) << "_createLogContentsFile open failed" << tempFile.errorString();
2737 return QString();
2738 }
2739 (void) tempFile.write(_mockLinkFTP->logFileContents(logName));
2740 tempFile.close();
2741 return tempFile.fileName();
2742}
2743
2744void MockLink::_handleLogRequestData(const mavlink_message_t &msg)
2745{
2746 mavlink_log_request_data_t request{};
2747 mavlink_msg_log_request_data_decode(&msg, &request);
2748
2749 // Serialize with _logDownloadWorker which reads this state every 2ms on the worker thread
2750 QMutexLocker locker(&_logDownloadMutex);
2751
2752 const QList<MockLinkFTP::LogFile> logFiles = _logsErased ? QList<MockLinkFTP::LogFile>() : _mockLinkFTP->logFiles();
2753 if (!logFiles.isEmpty()) {
2754 // Serve the simulated FTP log files so LOG_ENTRY/LOG_REQUEST_DATA stay consistent with the FTP transport
2755 if (request.id >= logFiles.count()) {
2756 qCWarning(MockLinkLog) << "_handleLogRequestData id out of range:" << request.id;
2757 return;
2758 }
2759 if (_logDownloadFilename.isEmpty() || (_logDownloadId != request.id)) {
2760 _logDownloadFilename = _createLogContentsFile(logFiles[request.id].name);
2761 _logDownloadId = request.id;
2762 _logDownloadSize = static_cast<uint32_t>(logFiles[request.id].size);
2763 }
2764 } else {
2765#ifdef QGC_UNITTEST_BUILD
2766 if (_logDownloadFilename.isEmpty()) {
2767 _logDownloadFilename = _createRandomFile(_logDownloadFileSize);
2768 }
2769#endif
2770 if (request.id != _logDownloadLogId) {
2771 qCWarning(MockLinkLog) << "_handleLogRequestData id must be" << _logDownloadLogId;
2772 return;
2773 }
2774 _logDownloadId = _logDownloadLogId;
2775 _logDownloadSize = _logDownloadFileSize;
2776 }
2777
2778 if (request.ofs > (_logDownloadSize - 1)) {
2779 qCWarning(MockLinkLog) << "_handleLogRequestData offset past end of file request.ofs:size" << request.ofs << _logDownloadSize;
2780 return;
2781 }
2782
2783 // This will trigger _logDownloadWorker to send data
2784 _logDownloadCurrentOffset = request.ofs;
2785 if (request.ofs + request.count > _logDownloadSize) {
2786 request.count = _logDownloadSize - request.ofs;
2787 }
2788 _logDownloadBytesRemaining = request.count;
2789}
2790
2791void MockLink::_logDownloadWorker()
2792{
2793 // Runs every 2ms (500Hz on worker thread). Must protect shared state modified by main thread.
2794 // Without lock: main could write new offset/count while we're reading, causing corrupted downloads.
2795 QMutexLocker locker(&_logDownloadMutex);
2796 if (_logDownloadBytesRemaining == 0) {
2797 return;
2798 }
2799
2800 QFile file(_logDownloadFilename);
2801 if (!file.open(QIODevice::ReadOnly)) {
2802 qCWarning(MockLinkLog) << "_logDownloadWorker open failed" << file.errorString();
2803 _logDownloadBytesRemaining = 0; // Abort transfer on I/O failure
2804 return;
2805 }
2806
2807 uint8_t buffer[MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN]{};
2808
2809 const qint64 bytesToRead = qMin(_logDownloadBytesRemaining, (uint32_t)MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN);
2810 if (!file.seek(_logDownloadCurrentOffset)) {
2811 qCWarning(MockLinkLog) << "_logDownloadWorker seek failed - offset:" << _logDownloadCurrentOffset << file.errorString();
2812 _logDownloadBytesRemaining = 0; // Abort transfer on I/O failure
2813 return;
2814 }
2815 if (file.read(reinterpret_cast<char*>(buffer), bytesToRead) != bytesToRead) {
2816 qCWarning(MockLinkLog) << "_logDownloadWorker read failed - bytesToRead:" << bytesToRead << file.errorString();
2817 _logDownloadBytesRemaining = 0; // Abort transfer on I/O failure
2818 return;
2819 }
2820
2821 qCDebug(MockLinkLog) << "_logDownloadWorker" << _logDownloadCurrentOffset << _logDownloadBytesRemaining;
2822
2823 mavlink_message_t responseMsg{};
2824 (void) mavlink_msg_log_data_pack_chan(
2825 _vehicleSystemId,
2826 _vehicleComponentId,
2827 _outgoingMavlinkChannel,
2828 &responseMsg,
2829 _logDownloadId,
2830 _logDownloadCurrentOffset,
2831 bytesToRead,
2832 &buffer[0]
2833 );
2834 respondWithMavlinkMessage(responseMsg);
2835
2836 _logDownloadCurrentOffset += bytesToRead;
2837 _logDownloadBytesRemaining -= bytesToRead;
2838
2839 file.close();
2840}
2841
2842void MockLink::_sendADSBVehicles()
2843{
2844 for (int i = 0; i < _adsbVehicles.size(); ++i) {
2845 // Slightly change the direction to simulate different paths
2846 _adsbVehicles[i].angle += (i + 1); // Vary the change to make each path unique
2847
2848 // Move each vehicle by a smaller distance to simulate slower speed
2849 _adsbVehicles[i].coordinate = _adsbVehicles[i].coordinate.atDistanceAndAzimuth(5, _adsbVehicles[i].angle); // 50 meters per update for slower speed
2850
2851 // Simulate slight variations in altitude
2852 _adsbVehicles[i].altitude += (i % 2 == 0 ? 0.5 : -0.5); // Increase or decrease altitude
2853
2854 QByteArray callsign = QString("N12345%1").arg(i, 2, 10, QChar('0')).toLatin1();
2855 callsign.resize(MAVLINK_MSG_ADSB_VEHICLE_FIELD_CALLSIGN_LEN);
2856
2857 // Prepare and send MAVLink message for each vehicle
2858 mavlink_message_t responseMsg{};
2859 (void) mavlink_msg_adsb_vehicle_pack_chan(
2860 _vehicleSystemId,
2861 _vehicleComponentId,
2862 _outgoingMavlinkChannel,
2863 &responseMsg,
2864 12345 + i, // Unique ICAO address for each vehicle
2865 _adsbVehicles[i].coordinate.latitude() * 1e7,
2866 _adsbVehicles[i].coordinate.longitude() * 1e7,
2867 ADSB_ALTITUDE_TYPE_GEOMETRIC,
2868 _adsbVehicles[i].altitude * 1000, // Altitude in millimeters
2869 // Use the current angle as heading
2870 static_cast<uint16_t>(_adsbVehicles[i].angle * 100), // Heading in centidegrees
2871 0, 0, // Horizontal/Vertical velocity
2872 callsign.constData(), // Unique callsign
2873 ADSB_EMITTER_TYPE_ROTOCRAFT,
2874 1, // Seconds since last communication
2875 ADSB_FLAGS_VALID_COORDS | ADSB_FLAGS_VALID_ALTITUDE | ADSB_FLAGS_VALID_HEADING | ADSB_FLAGS_VALID_CALLSIGN | ADSB_FLAGS_SIMULATED,
2876 0 // Squawk code
2877 );
2878 respondWithMavlinkMessage(responseMsg);
2879 }
2880}
2881
2882void MockLink::_moveADSBVehicle(int vehicleIndex)
2883{
2884 _adsbAngles[vehicleIndex] += 10; // Increment angle for smoother movement
2885 QGeoCoordinate &coord = _adsbVehicleCoordinates[vehicleIndex];
2886
2887 // Update the position based on the new angle
2888 coord = QGeoCoordinate(coord.latitude(), coord.longitude()).atDistanceAndAzimuth(500, _adsbAngles[vehicleIndex]);
2889 coord.setAltitude(100); // Keeping altitude constant for simplicity
2890}
2891
2892void MockLink::_handleRequestMessageAutopilotVersion(const mavlink_command_long_t &/*request*/, bool &accepted)
2893{
2894 accepted = true;
2895
2896 switch (_failureMode) {
2898 break;
2900 accepted = false;
2901 return;
2903 accepted = true;
2904 return;
2905 default:
2906 break;
2907 }
2908
2909 _respondWithAutopilotVersion();
2910}
2911
2912void MockLink::_handleRequestMessageDebug(const mavlink_command_long_t &/*request*/, bool &accepted, bool &noAck)
2913{
2914 accepted = true;
2915 noAck = false;
2916
2917 switch (_requestMessageFailureMode) {
2919 break;
2921 return;
2923 accepted = false;
2924 return;
2926 accepted = false;
2927 noAck = true;
2928 return;
2929 }
2930
2931 mavlink_message_t responseMsg{};
2932 (void) mavlink_msg_debug_pack_chan(
2933 _vehicleSystemId,
2934 _vehicleComponentId,
2935 _outgoingMavlinkChannel,
2936 &responseMsg,
2937 0, 0, 0
2938 );
2939 respondWithMavlinkMessage(responseMsg);
2940}
2941
2942void MockLink::_handleRequestMessageAvailableModes(const mavlink_command_long_t &request, bool &accepted)
2943{
2944 accepted = true;
2945
2946 // Thread-safe access: Check-then-set pattern must be atomic. Worker increments index every 2ms,
2947 // so check for "already running" and start/stop operations must serialize to prevent race where
2948 // main reads false, worker increments, main overwrites with different value -> lost update.
2949 QMutexLocker locker(&_availableModesWorkerMutex);
2950 if (request.param2 == 0) {
2951 // Request for available modes to be streamed out
2952 if (_availableModesWorkerNextModeIndex != 0) {
2953 qCWarning(MockLinkLog) << "MAVLINK_MSG_ID_AVAILABLE_MODES: _availableModesWorker already running - _availableModesWorkerNextModeIndex:" << _availableModesWorkerNextModeIndex;
2954 accepted = false;
2955 return;
2956 }
2957 qCDebug(MockLinkLog) << "MAVLINK_MSG_ID_AVAILABLE_MODES: starting available modes sequence worker";
2958 _availableModesWorkerNextModeIndex = 1; // Start with the first mode in sequence (1-based index)
2959 } else {
2960 // Request for specific mode
2961 if (request.param2 > _availableFlightModes.count()) {
2962 qCWarning(MockLinkLog) << "MAVLINK_MSG_ID_AVAILABLE_MODES: requested mode index out of range" << request.param2 << _availableFlightModes.count();
2963 accepted = false;
2964 return;
2965 }
2966 qCDebug(MockLinkLog) << "MAVLINK_MSG_ID_AVAILABLE_MODES: received specific mode request for index" << request.param2;
2967 _availableModesWorkerNextModeIndex = -request.param2; // Negative index indicates a specific single mode request
2968 }
2969}
2970
2971void MockLink::_handleRequestMessage(const mavlink_command_long_t &request, bool &accepted, bool &noAck)
2972{
2973 accepted = false;
2974 noAck = false;
2975
2976 const uint32_t requestedMessageId = static_cast<uint32_t>(request.param1);
2977
2978 // Per-message-ID no-response injection: silently drop the request (no ACK, no message)
2979 {
2980 QMutexLocker locker(&_requestMessageNoResponseMutex);
2981 if (_requestMessageNoResponseIds.contains(requestedMessageId)) {
2982 noAck = true;
2983 return;
2984 }
2985 }
2986
2987 switch (static_cast<int>(request.param1)) {
2988 case MAVLINK_MSG_ID_AUTOPILOT_VERSION:
2989 _handleRequestMessageAutopilotVersion(request, accepted);
2990 break;
2991 case MAVLINK_MSG_ID_COMPONENT_METADATA:
2992 if (_firmwareType == MAV_AUTOPILOT_PX4) {
2993 _sendGeneralMetaData();
2994 accepted = true;
2995 }
2996 break;
2997 case MAVLINK_MSG_ID_DEBUG:
2998 _handleRequestMessageDebug(request, accepted, noAck);
2999 break;
3000 case MAVLINK_MSG_ID_AVAILABLE_MODES:
3001 _handleRequestMessageAvailableModes(request, accepted);
3002 break;
3003 }
3004}
3005
3006void MockLink::_sendGeneralMetaData()
3007{
3008 static constexpr const char metaDataURI[MAVLINK_MSG_COMPONENT_METADATA_FIELD_URI_LEN] = "mftp://[;comp=1]general.json";
3009
3010 mavlink_message_t responseMsg{};
3011 (void) mavlink_msg_component_metadata_pack_chan(
3012 _vehicleSystemId,
3013 _vehicleComponentId,
3014 _outgoingMavlinkChannel,
3015 &responseMsg,
3016 0, // time_boot_ms
3017 25436021, // general_metadata_file_crc (crc32 of General.MetaData.json)
3018 metaDataURI
3019 );
3020 respondWithMavlinkMessage(responseMsg);
3021}
3022
3023void MockLink::setRemoteIDArmStatus(uint8_t status, const QString& error)
3024{
3025 QMutexLocker locker(&_remoteIDArmStatusMutex);
3026 _remoteIDArmStatus = status;
3027 _remoteIDArmStatusError = error;
3028}
3029
3030void MockLink::_sendRemoteIDArmStatus()
3031{
3032 uint8_t armStatus;
3033 QByteArray errorUtf8;
3034 {
3035 QMutexLocker locker(&_remoteIDArmStatusMutex);
3036 armStatus = _remoteIDArmStatus;
3037 errorUtf8 = _remoteIDArmStatusError.toUtf8();
3038 }
3039
3040 char armStatusError[MAVLINK_MSG_OPEN_DRONE_ID_ARM_STATUS_FIELD_ERROR_LEN] = {};
3041 std::strncpy(armStatusError, errorUtf8.constData(), sizeof(armStatusError) - 1);
3042
3043 mavlink_message_t msg{};
3044 (void) mavlink_msg_open_drone_id_arm_status_pack_chan(
3045 _vehicleSystemId,
3046 MAV_COMP_ID_ODID_TXRX_1,
3047 static_cast<uint8_t>(_outgoingMavlinkChannel),
3048 &msg,
3049 armStatus,
3050 armStatusError
3051 );
3053}
3054
3055void MockLink::_sendEscInfo()
3056{
3057 // Send ESC_INFO for 4 motors starting at index 0.
3058 // count=4 and info bitmask=0x0F (all 4 online) makes the ESC indicator visible.
3059 static const uint16_t failureFlags[4] = {0, 0, 0, 0};
3060 static const uint32_t errorCount[4] = {0, 0, 0, 0};
3061 static const int16_t temperature[4] = {3000, 3000, 3000, 3000}; // centidegrees
3062
3063 mavlink_message_t msg{};
3064 (void) mavlink_msg_esc_info_pack_chan(
3065 _vehicleSystemId,
3066 _vehicleComponentId,
3067 _outgoingMavlinkChannel,
3068 &msg,
3069 0, // index: first group starts at 0
3070 static_cast<uint64_t>(_runningTime.elapsed()) * 1000, // time_usec
3071 0, // counter
3072 4, // count: 4 motors
3073 ESC_CONNECTION_TYPE_DSHOT, // connection_type
3074 0x0F, // info: bitmask — motors 0-3 online
3075 failureFlags,
3076 errorCount,
3077 temperature
3078 );
3080}
3081
3082void MockLink::_sendEscStatus()
3083{
3084 static const int32_t rpm[4] = {5000, 5000, 5000, 5000};
3085 static const float voltage[4] = {16.0f, 16.0f, 16.0f, 16.0f};
3086 static const float current[4] = {5.0f, 5.0f, 5.0f, 5.0f};
3087
3088 mavlink_message_t msg{};
3089 (void) mavlink_msg_esc_status_pack_chan(
3090 _vehicleSystemId,
3091 _vehicleComponentId,
3092 _outgoingMavlinkChannel,
3093 &msg,
3094 0, // index: first group
3095 static_cast<uint64_t>(_runningTime.elapsed()) * 1000, // time_usec
3096 rpm,
3097 voltage,
3098 current
3099 );
3101}
3102
3103void MockLink::_sendRadioStatus()
3104{
3105 // Send a RADIO_STATUS message to make the TelemetryRSSI indicator visible.
3106 // Any non-zero rssi value triggers showIndicator (TelemetryRSSIIndicator checks lrssi.rawValue != 0).
3107 mavlink_message_t msg{};
3108 (void) mavlink_msg_radio_status_pack_chan(
3109 _vehicleSystemId,
3110 _vehicleComponentId,
3111 _outgoingMavlinkChannel,
3112 &msg,
3113 100, // rssi: local signal strength
3114 100, // remrssi: remote signal strength
3115 50, // txbuf: transmit buffer fill (%)
3116 10, // noise: local background noise
3117 10, // remnoise: remote background noise
3118 0, // rxerrors
3119 0 // fixed
3120 );
3122}
3123
3125{
3126 _commLost = true;
3128}
3129
3131{
3132 return _mockLinkFTP;
3133}
3134
3135void MockLink::_sendAvailableMode(uint8_t modeIndexOneBased)
3136{
3137 if (modeIndexOneBased > _availableModesCount()) {
3138 qCWarning(MockLinkLog) << "modeIndexOneBased out of range" << modeIndexOneBased << _availableModesCount();
3139 return;
3140 }
3141
3142 qCDebug(MockLinkLog) << "_sendAvailableMode modeIndexOneBased:" << modeIndexOneBased;
3143
3144 const FlightMode_t &availableMode = _availableFlightModes[modeIndexOneBased - 1];
3145 char modeName[MAVLINK_MSG_AVAILABLE_MODES_FIELD_MODE_NAME_LEN] = {};
3146 std::strncpy(modeName, availableMode.name, sizeof(modeName) - 1);
3147
3148 mavlink_message_t msg{};
3149
3150 (void) mavlink_msg_available_modes_pack_chan(
3151 _vehicleSystemId,
3152 _vehicleComponentId,
3153 _outgoingMavlinkChannel,
3154 &msg,
3155 _availableModesCount(),
3156 modeIndexOneBased,
3157 availableMode.standard_mode,
3158 availableMode.custom_mode,
3159 availableMode.canBeSet ? 0 : MAV_MODE_PROPERTY_NOT_USER_SELECTABLE,
3160 modeName);
3162}
3163
3164void MockLink::_availableModesWorker()
3165{
3166 // Runs every 2ms (500Hz on worker thread). Reads and increments shared index modified by main.
3167 // Read-modify-write must be atomic to prevent lost updates or incorrect state transitions.
3168 QMutexLocker locker(&_availableModesWorkerMutex);
3169 if (_availableModesWorkerNextModeIndex == 0) {
3170 // Not active
3171 return;
3172 }
3173
3174 _sendAvailableMode(qAbs(_availableModesWorkerNextModeIndex));
3175
3176 if (_availableModesWorkerNextModeIndex < 0) {
3177 // Single mode request, stop worker
3178 _availableModesWorkerNextModeIndex = 0;
3179 } else if (++_availableModesWorkerNextModeIndex > _availableModesCount()) {
3180 // All modes sent, stop worker
3181 _availableModesWorkerNextModeIndex = 0;
3182 qCDebug(MockLinkLog) << "_availableModesWorker: all modes sent, stopping worker";
3183 }
3184}
3185
3186void MockLink::_sendAvailableModesMonitor()
3187{
3188 mavlink_message_t msg{};
3189
3190 (void) mavlink_msg_available_modes_monitor_pack_chan(
3191 _vehicleSystemId,
3192 _vehicleComponentId,
3193 _outgoingMavlinkChannel,
3194 &msg,
3195 _availableModesMonitorSeqNumber);
3197}
3198
3199int MockLink::_availableModesCount() const
3200{
3201 return _availableFlightModes.count() - (_availableModesMonitorSeqNumber == 0 ? 1 : 0); // Exclude the delayed mode
3202}
3203
3204// ---------------------------------------------------------------------------
3205// ArduPilot compass calibration simulation
3206//
3207// Driven by _apmCompassCalProgress: -1 = inactive, 0..100 = current pct.
3208// Main thread sets it to 0 to start and -1 to stop.
3209// Worker sends MAG_CAL_PROGRESS at ~10Hz (every 50 calls of 500Hz worker),
3210// then sends MAG_CAL_REPORT when pct reaches 100.
3211// ---------------------------------------------------------------------------
3213{
3214 QMutexLocker locker(&_apmCompassCalMutex);
3215 _apmStaleFailedMagCalReportStreaming = true;
3216 _apmCompassCalTickCount = 0;
3217}
3218
3220{
3221 QMutexLocker locker(&_apmCompassCalMutex);
3222 return _apmStaleFailedMagCalReportStreaming;
3223}
3224
3225void MockLink::_apmCompassCalWorker()
3226{
3227 if (_firmwareType != MAV_AUTOPILOT_ARDUPILOTMEGA) {
3228 return;
3229 }
3230
3231 QMutexLocker locker(&_apmCompassCalMutex);
3232 if ((_apmCompassCalProgress < 0) && !_apmStaleFailedMagCalReportStreaming) {
3233 return;
3234 }
3235
3236 // Tick ~10 Hz: advance every 50 calls of the 500 Hz worker
3237 if (++_apmCompassCalTickCount < 50) {
3238 return;
3239 }
3240 _apmCompassCalTickCount = 0;
3241
3242 if (_apmStaleFailedMagCalReportStreaming) {
3243 // Simulate ArduPilot streaming the report from a previously failed cal until cancelled
3244 mavlink_message_t msg{};
3245 mavlink_mag_cal_report_t report{};
3246 report.compass_id = 0;
3247 report.cal_mask = 0x01;
3248 report.cal_status = MAG_CAL_FAILED;
3249 report.fitness = 999.0f;
3250 (void) mavlink_msg_mag_cal_report_encode_chan(
3251 _vehicleSystemId, _vehicleComponentId, _outgoingMavlinkChannel, &msg, &report);
3253 return;
3254 }
3255
3256 const int pct = _apmCompassCalProgress;
3257
3258 // Send MAG_CAL_PROGRESS for all 3 active compasses
3259 mavlink_message_t msg{};
3260 for (uint8_t id = 0; id < 3; ++id) {
3261 mavlink_mag_cal_progress_t progress{};
3262 progress.compass_id = id;
3263 progress.cal_mask = 0x07; // all 3 compasses
3264 progress.cal_status = MAG_CAL_RUNNING_STEP_ONE;
3265 progress.completion_pct = static_cast<uint8_t>(qMin(pct, 100));
3266 (void) mavlink_msg_mag_cal_progress_encode_chan(
3267 _vehicleSystemId, _vehicleComponentId, _outgoingMavlinkChannel, &msg, &progress);
3269 }
3270
3271 if (pct >= 100) {
3272 // Send MAG_CAL_REPORT for all 3 compasses
3273 for (uint8_t id = 0; id < 3; ++id) {
3274 mavlink_mag_cal_report_t report{};
3275 report.compass_id = id;
3276 report.cal_mask = 0x07;
3277 report.cal_status = MAG_CAL_SUCCESS;
3278 report.fitness = 0.5f;
3279 (void) mavlink_msg_mag_cal_report_encode_chan(
3280 _vehicleSystemId, _vehicleComponentId, _outgoingMavlinkChannel, &msg, &report);
3282 }
3283
3284 // Write non-zero offsets so QGC sees all compasses as calibrated after refresh.
3285 // _mapParamName2Value is owned by MockLink's main thread; dispatch the write there
3286 // to avoid a data race with concurrent param reads on the main thread.
3287 (void) QMetaObject::invokeMethod(this, [this] {
3288 constexpr int compId = MAV_COMP_ID_AUTOPILOT1;
3289 _mapParamName2Value[compId][QStringLiteral("COMPASS_OFS_X")] = QVariant(10.0f);
3290 _mapParamName2Value[compId][QStringLiteral("COMPASS_OFS_Y")] = QVariant(10.0f);
3291 _mapParamName2Value[compId][QStringLiteral("COMPASS_OFS_Z")] = QVariant(10.0f);
3292 _mapParamName2Value[compId][QStringLiteral("COMPASS_OFS2_X")] = QVariant(10.0f);
3293 _mapParamName2Value[compId][QStringLiteral("COMPASS_OFS2_Y")] = QVariant(10.0f);
3294 _mapParamName2Value[compId][QStringLiteral("COMPASS_OFS2_Z")] = QVariant(10.0f);
3295 _mapParamName2Value[compId][QStringLiteral("COMPASS_OFS3_X")] = QVariant(10.0f);
3296 _mapParamName2Value[compId][QStringLiteral("COMPASS_OFS3_Y")] = QVariant(10.0f);
3297 _mapParamName2Value[compId][QStringLiteral("COMPASS_OFS3_Z")] = QVariant(10.0f);
3298 }, Qt::QueuedConnection);
3299
3300 _apmCompassCalProgress = -1; // deactivate
3301 } else {
3302 _apmCompassCalProgress = qMin(pct + 5, 100);
3303 }
3304}
3305
3306// ---------------------------------------------------------------------------
3307// ArduPilot full accelerometer calibration simulation
3308//
3309// The firmware sends COMMAND_LONG(MAV_CMD_ACCELCAL_VEHICLE_POS, pos) to QGC
3310// and waits for a COMMAND_ACK (the "Next" button press) before advancing.
3311// State: _apmAccelCalPosIndex -1=inactive, 0..5=current pose, 6=done.
3312// ---------------------------------------------------------------------------
3313void MockLink::_apmAccelCalWorker()
3314{
3315 if (_firmwareType != MAV_AUTOPILOT_ARDUPILOTMEGA) {
3316 return;
3317 }
3318
3319 QMutexLocker locker(&_apmAccelCalMutex);
3320 if (_apmAccelCalPosIndex < 0) {
3321 return;
3322 }
3323
3324 if (_apmAccelCalPosIndex == 6) {
3325 // All poses done: send SUCCESS
3326 mavlink_message_t msg{};
3328 cmd.target_system = 255; // GCS
3329 cmd.target_component = MAV_COMP_ID_MISSIONPLANNER;
3330 cmd.command = MAV_CMD_ACCELCAL_VEHICLE_POS;
3331 cmd.param1 = static_cast<float>(ACCELCAL_VEHICLE_POS_SUCCESS);
3332 (void) mavlink_msg_command_long_encode_chan(
3333 _vehicleSystemId, _vehicleComponentId, _outgoingMavlinkChannel, &msg, &cmd);
3335
3336 // Write non-zero accel offsets.
3337 // _mapParamName2Value is owned by MockLink's main thread; dispatch the write there
3338 // to avoid a data race with concurrent param reads on the main thread.
3339 (void) QMetaObject::invokeMethod(this, [this] {
3340 constexpr int compId = MAV_COMP_ID_AUTOPILOT1;
3341 _mapParamName2Value[compId][QStringLiteral("INS_ACCOFFS_X")] = QVariant(0.1f);
3342 _mapParamName2Value[compId][QStringLiteral("INS_ACCOFFS_Y")] = QVariant(0.1f);
3343 _mapParamName2Value[compId][QStringLiteral("INS_ACCOFFS_Z")] = QVariant(0.1f);
3344 }, Qt::QueuedConnection);
3345
3346 _apmAccelCalPosIndex = -1;
3347 return;
3348 }
3349
3350 // Send the current position request once; wait for Next ack before advancing
3351 if (!_apmAccelCalGotAck) {
3352 // Throttle re-sends to ~10 Hz
3353 if (++_apmAccelCalTickCount < 50) {
3354 return;
3355 }
3356 _apmAccelCalTickCount = 0;
3357
3358 mavlink_message_t msg{};
3360 cmd.target_system = 255;
3361 cmd.target_component = MAV_COMP_ID_MISSIONPLANNER;
3362 cmd.command = MAV_CMD_ACCELCAL_VEHICLE_POS;
3363 cmd.param1 = static_cast<float>(kAPMAccelCalPosSequence[_apmAccelCalPosIndex]);
3364 (void) mavlink_msg_command_long_encode_chan(
3365 _vehicleSystemId, _vehicleComponentId, _outgoingMavlinkChannel, &msg, &cmd);
3367 } else {
3368 // Ack received — advance to next pose
3369 _apmAccelCalGotAck = false;
3370 _apmAccelCalPosIndex++;
3371 }
3372}
Config config
std::shared_ptr< LinkConfiguration > SharedLinkConfigurationPtr
mavlink_status_t * mavlink_get_channel_status(uint8_t chan)
Definition QGCMAVLink.cc:53
struct __mavlink_setup_signing_t mavlink_setup_signing_t
Error error
struct __mavlink_message mavlink_message_t
#define QGC_LOGGING_CATEGORY(name, categoryStr)
struct __mavlink_command_ack_t mavlink_command_ack_t
struct param_union mavlink_param_union_t
struct __mavlink_command_long_t mavlink_command_long_t
static size_t typeToSize(ValueType_t type)
void setDynamic(bool dynamic=true)
Set if this is this a dynamic configuration. (decided at runtime)
QString name() const
The link interface defines the interface for all links used to communicate with the ground station ap...
void bytesReceived(LinkInterface *link, const QByteArray &data)
void disconnected()
virtual void _freeMavlinkChannel()
void _connectionRemoved()
bool mavlinkChannelIsSet() const
virtual bool _allocateMavlinkChannel()
void connected()
SharedLinkConfigurationPtr linkConfiguration()
SharedLinkConfigurationPtr addConfiguration(LinkConfiguration *config)
uint8_t allocateMavlinkChannel()
static LinkManager * instance()
void freeMavlinkChannel(uint8_t channel)
static constexpr uint8_t invalidMavlinkChannel()
static int getComponentId()
static MAVLinkProtocol * instance()
int getSystemId() const
bool preloadMission() const
@ VideoStreamRtpUdpH265
RTP/UDP H.265 -> udp265://.
@ VideoStreamNone
No stream served.
@ VideoStreamRtpUdpH264
RTP/UDP H.264 -> udp://.
@ VideoStreamRtspH264
RTSP H.264 -> rtsp://.
@ VideoStreamMpegTsTcp
MPEG-TS over TCP -> tcp://.
@ VideoStreamMpegTsUdp
MPEG-TS over UDP -> mpegts://.
void setVehicleType(MAV_TYPE vehicleType)
@ FailInitialConnectRequestMessageAutopilotVersionLost
REQUEST_MESSAGE:AUTOPILOT_VERSION success, AUTOPILOT_VERSION never sent.
@ FailMissingParamOnInitialRequest
Not all params are sent on initial request, should still succeed since QGC will re-query missing para...
@ FailMissingParamOnAllRequests
Not all params are sent on initial request, QGC retries will fail as well.
@ FailInitialConnectRequestMessageAutopilotVersionFailure
REQUEST_MESSAGE:AUTOPILOT_VERSION returns failure.
@ FailParamNoResponseToRequestList
Do not respond to PARAM_REQUEST_LIST.
void setFailureMode(FailureMode_t failureMode)
void setEnableProximity(bool enableProximity)
void setApmStartFreshParams(bool apmStartFreshParams)
void setEnableCamera(bool enableCamera)
void setEnableGimbal(bool enableGimbal)
void setSendStatusText(bool sendStatusText)
bool cameraHasVideoStream() const
void setFtpCapability(bool ftpCapability)
void setFirmwareType(MAV_AUTOPILOT firmwareType)
void setVideoStreamType(int value)
void setPreloadMission(bool preloadMission)
void setStayMavlinkV1(bool stayV1)
Simulates MAVLink Camera Protocol v2 components for MockLink.
void sendCameraHeartbeats()
Send heartbeats for all simulated camera components (call from 1Hz tasks)
bool handleMavlinkMessage(const mavlink_message_t &msg)
void run10HzTasks()
Update camera states (call from 10Hz tasks)
Mock implementation of Mavlink FTP server.
Definition MockLinkFTP.h:16
QList< LogFile > logFiles() const
Returns the log files served from the @MAV_LOG virtual directory.
Definition MockLinkFTP.h:39
void setLogFiles(const QList< LogFile > &logFiles)
void mavlinkMessageReceived(const mavlink_message_t &message)
Called to handle an FTP message.
QByteArray logFileContents(const QString &name) const
Simulates MAVLink Gimbal Manager Protocol for MockLink.
void run1HzTasks()
Send periodic gimbal status messages (call from 1Hz tasks)
bool handleMavlinkMessage(const mavlink_message_t &msg)
bool handleMavlinkMessage(const mavlink_message_t &msg)
Simulates the PX4 commander magnetometer and accelerometer calibration protocols for MockLink.
void run10HzTasks()
Called by MockLink::run10HzTasks on the worker thread.
Worker class that runs periodic tasks for MockLink simulation.
Serves a synthetic (videotestsrc) live video stream for MockLink so the GStreamer receive pipeline ca...
@ MpegTsTcp
MPEG-TS over TCP -> tcp://.
@ RtpUdpH265
RTP/H.265 over UDP -> udp265://.
@ RtpUdpH264
RTP/H.264 over UDP -> udp://.
@ RtspH264
RTSP (H.264) -> rtsp://.
@ MpegTsUdp
MPEG-TS over UDP -> mpegts://.
static FactMetaData::ValueType_t mavTypeToFactType(MAV_PARAM_TYPE mavType)
uint64_t currentSigningTimestampTicks()
Current signing timestamp in 10µs ticks since 2015-01-01.
bool insecureConnectionAcceptUnsignedCallback(const mavlink_status_t *status, uint32_t message_id)
quint32 crc32(const quint8 *src, unsigned len, unsigned state)
Definition QGCMath.cc:100
bool runningUnitTests()
void secureZero(void *data, size_t size)
@ PX4_CUSTOM_MAIN_MODE_AUTO
@ PX4_CUSTOM_SUB_MODE_AUTO_RESERVED_DO_NOT_USE