QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
MockLinkGimbal.cc
Go to the documentation of this file.
1#include "MockLinkGimbal.h"
2#include "MAVLinkLib.h"
3#include "MockLink.h"
6
7#include <QtCore/QDateTime>
8#include <QtMath>
9
10#include <cmath>
11
12QGC_LOGGING_CATEGORY(MockLinkGimbalLog, "Comms.MockLink.MockLinkGimbal")
13
15 MockLink *mockLink,
16 bool hasRollAxis,
17 bool hasPitchAxis,
18 bool hasYawAxis,
19 bool hasYawFollow,
20 bool hasYawLock,
21 bool hasRetract,
22 bool hasNeutral,
23 uint8_t gimbalDeviceId)
24 : _mockLink(mockLink)
25 , _gimbalDeviceId(gimbalDeviceId)
26 , _hasRollAxis(hasRollAxis)
27 , _hasPitchAxis(hasPitchAxis)
28 , _hasYawAxis(hasYawAxis)
29 , _hasYawFollow(hasYawFollow)
30 , _hasYawLock(hasYawLock)
31 , _hasRetract(hasRetract)
32 , _hasNeutral(hasNeutral)
33{
34}
35
37{
38 // Runs every 1s (1Hz on worker thread). Reads intervals and gimbal state main thread modifies.
39 // Must serialize access to prevent:
40 // - Auto-movement overwriting manual control commands
41 // - Reading stale/inconsistent interval values for status transmission
42 QMutexLocker locker(&_stateMutex);
43 const qint64 nowMs = QDateTime::currentMSecsSinceEpoch();
44
45 // Send GIMBAL_MANAGER_STATUS if interval is set
46 if (_managerStatusIntervalUs > 0) {
47 const qint64 intervalMs = _managerStatusIntervalUs / 1000;
48 if ((nowMs - _managerStatusLastSentMs) >= intervalMs) {
49 _sendGimbalManagerStatus();
50 _managerStatusLastSentMs = nowMs;
51 }
52 }
53
54 // Send GIMBAL_DEVICE_ATTITUDE_STATUS if interval is set
55 if (_deviceAttitudeStatusIntervalUs > 0) {
56 const qint64 intervalMs = _deviceAttitudeStatusIntervalUs / 1000;
57 if ((nowMs - _deviceAttitudeStatusLastSentMs) >= intervalMs) {
58 // Only simulate automatic movement if not under manual control
59 if (!_manualControl) {
60 // Simulate simple gimbal movement (slow sine wave)
61 const qint64 secondsSinceEpoch = nowMs / 1000;
62 _pitch = 10.0f * qSin(secondsSinceEpoch * 0.1); // ±10° pitch oscillation
63 _yaw = 15.0f * qCos(secondsSinceEpoch * 0.15); // ±15° yaw oscillation
64 }
65
66 _sendGimbalDeviceAttitudeStatus();
67 _deviceAttitudeStatusLastSentMs = nowMs;
68 }
69 }
70}
71
73{
74 if (msg.msgid == MAVLINK_MSG_ID_GIMBAL_MANAGER_SET_ATTITUDE) {
75 _handleGimbalManagerSetAttitude(msg);
76 return true;
77 }
78
79 if (msg.msgid != MAVLINK_MSG_ID_COMMAND_LONG) {
80 return false;
81 }
82
83 mavlink_command_long_t request{};
84 mavlink_msg_command_long_decode(&msg, &request);
85
86 // Handle gimbal-specific commands
87 switch (request.command) {
88 case MAV_CMD_SET_MESSAGE_INTERVAL:
89 if (_handleSetMessageInterval(request)) {
90 _sendCommandAck(request.command, MAV_RESULT_ACCEPTED, request.target_component);
91 return true;
92 }
93 return false;
94
95 case MAV_CMD_REQUEST_MESSAGE: {
96 if (static_cast<int>(request.param1) == MAVLINK_MSG_ID_GIMBAL_DEVICE_INFORMATION) {
97 _sendCommandAck(request.command, MAV_RESULT_ACCEPTED, request.target_component);
98 _handleRequestMessage(request);
99 return true;
100 }
101 if (static_cast<int>(request.param1) != MAVLINK_MSG_ID_GIMBAL_MANAGER_INFORMATION) {
102 return false;
103 }
104 InformationResponse response;
105 {
106 QMutexLocker locker(&_stateMutex);
107 response = _informationResponse;
108 }
109 switch (response) {
111 _sendCommandAck(request.command, MAV_RESULT_ACCEPTED, request.target_component);
112 _handleRequestMessage(request);
113 break;
115 _sendCommandAck(request.command, MAV_RESULT_UNSUPPORTED, request.target_component);
116 break;
118 break;
119 }
120 return true;
121 }
122
123 case MAV_CMD_DO_GIMBAL_MANAGER_PITCHYAW:
124 if (_handleGimbalManagerPitchYaw(request)) {
125 _sendCommandAck(request.command, MAV_RESULT_ACCEPTED, request.target_component);
126 return true;
127 }
128 _sendCommandAck(request.command, MAV_RESULT_DENIED, request.target_component);
129 return true;
130
131 case MAV_CMD_DO_GIMBAL_MANAGER_CONFIGURE:
132 if (_handleGimbalManagerConfigure(request, msg.sysid, msg.compid)) {
133 _sendCommandAck(request.command, MAV_RESULT_ACCEPTED, request.target_component);
134 return true;
135 }
136 _sendCommandAck(request.command, MAV_RESULT_DENIED, request.target_component);
137 return true;
138
139 default:
140 return false;
141 }
142}
143
144void MockLinkGimbal::_sendGimbalDeviceInformation()
145{
146 qCDebug(MockLinkGimbalLog) << "Sending GIMBAL_DEVICE_INFORMATION - vendor:" << kVendorName << "model:" << kModelName
147 << "deviceId:" << _attitudeDeviceIdField();
148
149 // Packer copies the full fixed-width fields, so short literals would read out of bounds
150 char vendorName[MAVLINK_MSG_GIMBAL_DEVICE_INFORMATION_FIELD_VENDOR_NAME_LEN + 1] = {};
151 char modelName[MAVLINK_MSG_GIMBAL_DEVICE_INFORMATION_FIELD_MODEL_NAME_LEN + 1] = {};
152 char customName[MAVLINK_MSG_GIMBAL_DEVICE_INFORMATION_FIELD_CUSTOM_NAME_LEN + 1] = {};
153 (void) strncpy(vendorName, kVendorName, MAVLINK_MSG_GIMBAL_DEVICE_INFORMATION_FIELD_VENDOR_NAME_LEN);
154 (void) strncpy(modelName, kModelName, MAVLINK_MSG_GIMBAL_DEVICE_INFORMATION_FIELD_MODEL_NAME_LEN);
155
156 mavlink_message_t msg{};
157 (void) mavlink_msg_gimbal_device_information_pack_chan(_mockLink->vehicleId(), _attitudeSourceCompid(),
158 _mockLink->outgoingMavlinkChannel(), &msg,
159 0, // time_boot_ms
160 vendorName, modelName, customName,
161 0, // firmware_version
162 0, // hardware_version
163 0, // uid
164 0, // cap_flags
165 0, // custom_cap_flags
166 -45, 45, -45, 45, -180, 180, _attitudeDeviceIdField(),
167 0); // cap_flags2
168 _mockLink->respondWithMavlinkMessage(msg);
169}
170
171void MockLinkGimbal::_sendCommandAck(uint16_t command, uint8_t result, uint8_t sourceCompId)
172{
173 QString commandName = MissionCommandTree::instance()->rawName(static_cast<MAV_CMD>(command));
174 qCDebug(MockLinkGimbalLog) << "Sending command ACK -" << QString("%1(%2)").arg(commandName).arg(command) << "result:" << (result == MAV_RESULT_ACCEPTED ? "ACCEPTED" : result == MAV_RESULT_DENIED ? "DENIED" : QString::number(result));
175
176 mavlink_message_t msg{};
177 (void) mavlink_msg_command_ack_pack_chan(
178 _mockLink->vehicleId(),
179 sourceCompId,
180 _mockLink->outgoingMavlinkChannel(),
181 &msg,
182 command,
183 result,
184 0, // progress
185 0, // result_param2
186 0, // target_system
187 0 // target_component
188 );
189 _mockLink->respondWithMavlinkMessage(msg);
190}
191
192bool MockLinkGimbal::_handleSetMessageInterval(const mavlink_command_long_t &request)
193{
194 const int msgId = static_cast<int>(request.param1);
195 const int intervalUs = static_cast<int>(request.param2);
196 const mavlink_message_info_t* info = mavlink_get_message_info_by_id(static_cast<uint32_t>(msgId));
197 QString msgName = info ? info->name : QString::number(msgId);
198
199 qCDebug(MockLinkGimbalLog) << "SET_MESSAGE_INTERVAL -" << QString("%1(%2)").arg(msgName).arg(msgId) << "intervalUs:" << intervalUs;
200
201 // Handle interval values per MAVLink spec:
202 // -1 = disable
203 // 0 = default rate
204 // >0 = interval in microseconds
205 int effectiveInterval = intervalUs;
206 if (intervalUs == 0) {
207 effectiveInterval = kDefaultIntervalUs;
208 } else if (intervalUs < -1) {
209 // Invalid interval
210 return false;
211 }
212
213 // Thread-safe access: Main thread writing interval while worker thread reads every 1s.
214 // Serialize to avoid worker using stale interval for message transmission logic.
215 QMutexLocker locker(&_stateMutex);
216 if (msgId == MAVLINK_MSG_ID_GIMBAL_MANAGER_STATUS) {
217 _statusIntervalRequests.append(intervalUs);
218 if (_ignoreStatusIntervalRequests > 0) {
219 --_ignoreStatusIntervalRequests;
220 qCDebug(MockLinkGimbalLog) << msgName << "interval request ignored, remaining to ignore:" << _ignoreStatusIntervalRequests;
221 return true;
222 }
223 _managerStatusIntervalUs = effectiveInterval;
224 qCDebug(MockLinkGimbalLog) << msgName << "interval set to" << effectiveInterval << "us";
225 return true;
226 } else if (msgId == MAVLINK_MSG_ID_GIMBAL_DEVICE_ATTITUDE_STATUS) {
227 _deviceAttitudeStatusIntervalUs = effectiveInterval;
228 qCDebug(MockLinkGimbalLog) << msgName << "interval set to" << effectiveInterval << "us";
229 return true;
230 }
231 return false;
232}
233
234bool MockLinkGimbal::_handleRequestMessage(const mavlink_command_long_t &request)
235{
236 const int msgId = static_cast<int>(request.param1);
237 const mavlink_message_info_t* info = mavlink_get_message_info_by_id(static_cast<uint32_t>(msgId));
238 QString msgName = info ? info->name : QString::number(msgId);
239
240 if (msgId == MAVLINK_MSG_ID_GIMBAL_MANAGER_INFORMATION) {
241 qCDebug(MockLinkGimbalLog) << "REQUEST_MESSAGE -" << QString("%1(%2)").arg(msgName).arg(msgId);
242 _sendGimbalManagerInformation();
243 return true;
244 }
245
246 if (msgId == MAVLINK_MSG_ID_GIMBAL_DEVICE_INFORMATION) {
247 qCDebug(MockLinkGimbalLog) << "REQUEST_MESSAGE -" << QString("%1(%2)").arg(msgName).arg(msgId);
248 _sendGimbalDeviceInformation();
249 return true;
250 }
251
252 return false;
253}
254
255bool MockLinkGimbal::_handleGimbalManagerPitchYaw(const mavlink_command_long_t &request)
256{
257 // param1: pitch angle (degrees, positive: down, negative: up)
258 // param2: yaw angle (degrees, positive: to the right, negative: to the left)
259 // param3: pitch rate (deg/s)
260 // param4: yaw rate (deg/s)
261 // param5: flags (GIMBAL_MANAGER_FLAGS)
262 // param7: gimbal device id
263
264 const float requestedPitch = request.param1;
265 const float requestedYaw = request.param2;
266 const uint32_t flags = static_cast<uint32_t>(request.param5);
267
268 qCDebug(MockLinkGimbalLog) << "DO_GIMBAL_MANAGER_PITCHYAW - pitch:" << requestedPitch << "yaw:" << requestedYaw << "flags:" << flags;
269
270 QMutexLocker locker(&_stateMutex);
271 _lastPitchYawCommand.pitchDeg = requestedPitch;
272 _lastPitchYawCommand.yawDeg = requestedYaw;
273 _lastPitchYawCommand.pitchRateDegS = request.param3;
274 _lastPitchYawCommand.yawRateDegS = request.param4;
275 _lastPitchYawCommand.flags = flags;
276 _lastPitchYawCommand.deviceId = static_cast<uint8_t>(request.param7);
277 ++_lastPitchYawCommand.count;
278
279 // Flag-only commands (yaw lock / retract toggles) carry the current angles, so flags always apply
280 _yawLock = (flags & GIMBAL_MANAGER_FLAGS_YAW_LOCK) != 0;
281
282 // Check if this is a valid request (NaN means no change requested)
283 const bool updatePitch = !qIsNaN(requestedPitch);
284 const bool updateYaw = !qIsNaN(requestedYaw);
285 const bool updateRates = !qIsNaN(request.param3) || !qIsNaN(request.param4);
286
287 if (!updatePitch && !updateYaw && !updateRates) {
288 qCDebug(MockLinkGimbalLog) << "DO_GIMBAL_MANAGER_PITCHYAW - no valid pitch/yaw requested (both NaN)";
289 return false; // Nothing to do
290 }
291
292 // Apply limits based on gimbal capabilities
293 if (updatePitch && _hasPitchAxis) {
294 _pitch = qBound(-45.0f, requestedPitch, 45.0f);
295 _manualControl = true; // Switch to manual control mode
296 }
297
298 if (updateYaw && _hasYawAxis) {
299 // Same frame rules as GIMBAL_DEVICE_ATTITUDE_STATUS: explicit frame flag, else YAW_LOCK means earth frame
300 bool earthFrame = false;
301 if (flags & GIMBAL_MANAGER_FLAGS_YAW_IN_EARTH_FRAME) {
302 earthFrame = true;
303 } else if (!(flags & GIMBAL_MANAGER_FLAGS_YAW_IN_VEHICLE_FRAME)) {
304 earthFrame = _yawLock;
305 }
306 _yawInEarthFrame = earthFrame;
307
308 // _yaw is always body yaw; an earth-frame command is convertible whenever the mock has its own heading
309 float bodyYaw = qBound(-180.0f, requestedYaw, 180.0f);
310 if (earthFrame && _deltaYawMode != DeltaYawMode::Legacy) {
311 bodyYaw = std::remainder(requestedYaw - _deltaYawDeg, 360.0f);
312 } else if (earthFrame) {
313 qCDebug(MockLinkGimbalLog) << "earth-frame yaw command stored as body yaw: mock has no heading in Legacy mode";
314 }
315 _yaw = bodyYaw;
316 _manualControl = true; // Switch to manual control mode
317 }
318
319 qCDebug(MockLinkGimbalLog) << "Gimbal commanded to pitch:" << _pitch << "yaw:" << _yaw << "- switched to manual control";
320 return true;
321}
322
323bool MockLinkGimbal::_handleGimbalManagerConfigure(const mavlink_command_long_t &request, uint8_t senderSysid, uint8_t senderCompid)
324{
325 // param1/2: sysid/compid primary control, param3/4: sysid/compid secondary control
326 // Each field: 0 no one, -1 unchanged, -2 sender, -3 release if sender owns the pair, >0 explicit id
327 // param7: gimbal device id
328
329 QMutexLocker locker(&_stateMutex);
330 _applyControlPair(request.param1, request.param2, senderSysid, senderCompid,
331 _gimbalManagerSysidPrimary, _gimbalManagerCompidPrimary);
332 _applyControlPair(request.param3, request.param4, senderSysid, senderCompid,
333 _gimbalManagerSysidSecondary, _gimbalManagerCompidSecondary);
334
335 qCDebug(MockLinkGimbalLog) << "Gimbal manager configured - Primary:" << _gimbalManagerSysidPrimary
336 << "/" << _gimbalManagerCompidPrimary
337 << "Secondary:" << _gimbalManagerSysidSecondary
338 << "/" << _gimbalManagerCompidSecondary;
339 return true;
340}
341
342void MockLinkGimbal::_applyControlPair(float sysidValue, float compidValue, uint8_t senderSysid, uint8_t senderCompid,
343 uint8_t &targetSysid, uint8_t &targetCompid)
344{
345 // Release acts on the pair as a whole so a non-owner can never revoke or half-clear the real controller
346 if (sysidValue == -3.0f || compidValue == -3.0f) {
347 if (targetSysid == senderSysid && targetCompid == senderCompid) {
348 targetSysid = 0;
349 targetCompid = 0;
350 }
351 return;
352 }
353 _applyControlField(sysidValue, senderSysid, targetSysid);
354 _applyControlField(compidValue, senderCompid, targetCompid);
355}
356
357void MockLinkGimbal::_applyControlField(float value, uint8_t senderValue, uint8_t &target)
358{
359 if (qIsNaN(value) || value == -1.0f) {
360 return;
361 }
362 if (value == -2.0f) {
363 target = senderValue;
364 } else if (value >= 0.0f) {
365 target = static_cast<uint8_t>(value);
366 }
367}
368
369void MockLinkGimbal::_handleGimbalManagerSetAttitude(const mavlink_message_t &msg)
370{
371 mavlink_gimbal_manager_set_attitude_t setAttitude{};
372 mavlink_msg_gimbal_manager_set_attitude_decode(&msg, &setAttitude);
373
374 qCDebug(MockLinkGimbalLog) << "GIMBAL_MANAGER_SET_ATTITUDE - pitchRate:" << setAttitude.angular_velocity_y
375 << "yawRate:" << setAttitude.angular_velocity_z
376 << "flags:" << setAttitude.flags;
377
378 QMutexLocker locker(&_stateMutex);
379 _lastSetAttitudeCommand.pitchRateDegS = qRadiansToDegrees(setAttitude.angular_velocity_y);
380 _lastSetAttitudeCommand.yawRateDegS = qRadiansToDegrees(setAttitude.angular_velocity_z);
381 _lastSetAttitudeCommand.flags = setAttitude.flags;
382 ++_lastSetAttitudeCommand.count;
383}
384
385void MockLinkGimbal::setAttitudeDeg(float rollDeg, float pitchDeg, float yawDeg)
386{
387 QMutexLocker locker(&_stateMutex);
388 _roll = rollDeg;
389 _pitch = pitchDeg;
390 _yaw = yawDeg;
391 _manualControl = true;
392}
393
395{
396 QMutexLocker locker(&_stateMutex);
397 _deltaYawMode = mode;
398}
399
400void MockLinkGimbal::setDeltaYawDeg(float deltaYawDeg)
401{
402 QMutexLocker locker(&_stateMutex);
403 _deltaYawDeg = deltaYawDeg;
404}
405
407{
408 QMutexLocker locker(&_stateMutex);
409 _yawInEarthFrame = earthFrame;
410}
411
412void MockLinkGimbal::setPrimaryControl(uint8_t sysid, uint8_t compid)
413{
414 QMutexLocker locker(&_stateMutex);
415 _gimbalManagerSysidPrimary = sysid;
416 _gimbalManagerCompidPrimary = compid;
417}
418
420{
421 QMutexLocker locker(&_stateMutex);
422 _sendGimbalDeviceAttitudeStatus();
423}
424
426{
427 QMutexLocker locker(&_stateMutex);
428 _sendGimbalManagerStatus();
429}
430
432{
433 _sendGimbalManagerInformation(deviceIdField);
434}
435
437{
438 QMutexLocker locker(&_stateMutex);
439 _sendGimbalManagerStatus(deviceIdField);
440}
441
442void MockLinkGimbal::sendGimbalDeviceAttitudeStatusFrom(uint8_t sourceCompid, uint8_t deviceIdField)
443{
444 QMutexLocker locker(&_stateMutex);
445 _sendGimbalDeviceAttitudeStatus(sourceCompid, deviceIdField);
446}
447
449{
450 QMutexLocker locker(&_stateMutex);
451 _informationResponse = response;
452}
453
455{
456 QMutexLocker locker(&_stateMutex);
457 _ignoreStatusIntervalRequests = count;
458}
459
461{
462 QMutexLocker locker(&_stateMutex);
463 return _statusIntervalRequests;
464}
465
467{
468 QMutexLocker locker(&_stateMutex);
469 return _lastPitchYawCommand;
470}
471
473{
474 QMutexLocker locker(&_stateMutex);
475 return _lastSetAttitudeCommand;
476}
477
479{
480 QMutexLocker locker(&_stateMutex);
481 return _yawLock;
482}
483
484void MockLinkGimbal::_sendGimbalManagerStatus()
485{
486 _sendGimbalManagerStatus(_gimbalDeviceId);
487}
488
489void MockLinkGimbal::_sendGimbalManagerStatus(uint8_t deviceIdField)
490{
491 qCDebug(MockLinkGimbalLog) << "Sending GIMBAL_MANAGER_STATUS - deviceId:" << deviceIdField;
492
493 mavlink_message_t msg{};
494 (void) mavlink_msg_gimbal_manager_status_pack_chan(
495 _mockLink->vehicleId(),
496 MAV_COMP_ID_AUTOPILOT1,
497 _mockLink->outgoingMavlinkChannel(),
498 &msg,
499 0, // time_boot_ms
500 0, // flags
501 deviceIdField,
502 _gimbalManagerSysidPrimary,
503 _gimbalManagerCompidPrimary,
504 _gimbalManagerSysidSecondary,
505 _gimbalManagerCompidSecondary);
506 _mockLink->respondWithMavlinkMessage(msg);
507}
508
509void MockLinkGimbal::_sendGimbalDeviceAttitudeStatus()
510{
511 _sendGimbalDeviceAttitudeStatus(_attitudeSourceCompid(), _attitudeDeviceIdField());
512}
513
514void MockLinkGimbal::_sendGimbalDeviceAttitudeStatus(uint8_t sourceCompid, uint8_t deviceIdField)
515{
516 qCDebug(MockLinkGimbalLog) << "Sending GIMBAL_DEVICE_ATTITUDE_STATUS - pitch:" << _pitch << "yaw:" << _yaw
517 << "manual:" << _manualControl << "deltaYawMode:" << static_cast<int>(_deltaYawMode)
518 << "deltaYaw:" << _deltaYawDeg << "earthFrame:" << _yawInEarthFrame
519 << "from:" << sourceCompid << "deviceId:" << deviceIdField;
520
521 uint16_t flags = GIMBAL_DEVICE_FLAGS_NEUTRAL | GIMBAL_DEVICE_FLAGS_ROLL_LOCK | GIMBAL_DEVICE_FLAGS_PITCH_LOCK;
522 if (_yawLock) {
523 flags |= GIMBAL_DEVICE_FLAGS_YAW_LOCK;
524 }
525
526 float deltaYawRad = 0.0f;
527 switch (_deltaYawMode) {
529 break;
531 flags |= _yawInEarthFrame ? GIMBAL_DEVICE_FLAGS_YAW_IN_EARTH_FRAME : GIMBAL_DEVICE_FLAGS_YAW_IN_VEHICLE_FRAME;
532 deltaYawRad = NAN;
533 break;
535 flags |= _yawInEarthFrame ? GIMBAL_DEVICE_FLAGS_YAW_IN_EARTH_FRAME : GIMBAL_DEVICE_FLAGS_YAW_IN_VEHICLE_FRAME;
536 deltaYawRad = qDegreesToRadians(_deltaYawDeg);
537 break;
538 }
539
540 // _yaw is body yaw; earth-frame reports q_earth = q_delta_yaw * q_body per the message spec. _deltaYawDeg is
541 // the mock's true heading even in Unknown mode where it is transmitted as NaN.
542 const float reportedYawDeg = (_yawInEarthFrame && _deltaYawMode != DeltaYawMode::Legacy) ? _yaw + _deltaYawDeg : _yaw;
543
544 // Convert Euler angles (degrees) to quaternion
545 const float rollRad = qDegreesToRadians(_roll);
546 const float pitchRad = qDegreesToRadians(_pitch);
547 const float yawRad = qDegreesToRadians(reportedYawDeg);
548
549 const float cy = qCos(yawRad * 0.5f);
550 const float sy = qSin(yawRad * 0.5f);
551 const float cp = qCos(pitchRad * 0.5f);
552 const float sp = qSin(pitchRad * 0.5f);
553 const float cr = qCos(rollRad * 0.5f);
554 const float sr = qSin(rollRad * 0.5f);
555
556 float q[4];
557 q[0] = cr * cp * cy + sr * sp * sy; // w
558 q[1] = sr * cp * cy - cr * sp * sy; // x
559 q[2] = cr * sp * cy + sr * cp * sy; // y
560 q[3] = cr * cp * sy - sr * sp * cy; // z
561
562 mavlink_message_t msg{};
563 (void) mavlink_msg_gimbal_device_attitude_status_pack_chan(
564 _mockLink->vehicleId(),
565 sourceCompid,
566 _mockLink->outgoingMavlinkChannel(),
567 &msg,
568 0, 0, // target system, component
569 0, // time_boot_ms
570 flags,
571 q,
572 0.0f, 0.0f, 0.0f, // angular_velocity_x, y, z
573 0, // failure flags
574 deltaYawRad,
575 NAN, // delta_yaw_velocity unknown
576 deviceIdField);
577 _mockLink->respondWithMavlinkMessage(msg);
578}
579
580void MockLinkGimbal::_sendGimbalManagerInformation()
581{
582 _sendGimbalManagerInformation(_gimbalDeviceId);
583}
584
585void MockLinkGimbal::_sendGimbalManagerInformation(uint8_t deviceIdField)
586{
587 // Build capability flags bitmask based on configuration
588 uint32_t capFlags = 0;
589 if (_hasRollAxis) capFlags |= GIMBAL_MANAGER_CAP_FLAGS_HAS_ROLL_AXIS;
590 if (_hasPitchAxis) capFlags |= GIMBAL_MANAGER_CAP_FLAGS_HAS_PITCH_AXIS;
591 if (_hasYawAxis) capFlags |= GIMBAL_MANAGER_CAP_FLAGS_HAS_YAW_AXIS;
592 if (_hasYawFollow) capFlags |= GIMBAL_MANAGER_CAP_FLAGS_HAS_YAW_FOLLOW;
593 if (_hasYawLock) capFlags |= GIMBAL_MANAGER_CAP_FLAGS_HAS_YAW_LOCK;
594 if (_hasRetract) capFlags |= GIMBAL_MANAGER_CAP_FLAGS_HAS_RETRACT;
595 if (_hasNeutral) capFlags |= GIMBAL_MANAGER_CAP_FLAGS_HAS_NEUTRAL;
596
597 qCDebug(MockLinkGimbalLog) << "Sending GIMBAL_MANAGER_INFORMATION - capFlags:" << QString::number(capFlags, 16) << "deviceId:" << deviceIdField;
598
599 mavlink_message_t msg{};
600 (void) mavlink_msg_gimbal_manager_information_pack_chan(
601 _mockLink->vehicleId(),
602 MAV_COMP_ID_AUTOPILOT1,
603 _mockLink->outgoingMavlinkChannel(),
604 &msg,
605 0, // time_boot_ms
606 capFlags,
607 deviceIdField,
608 -45, 45,
609 -45, 45,
610 -180, 180);
611 _mockLink->respondWithMavlinkMessage(msg);
612}
struct __mavlink_message mavlink_message_t
#define QGC_LOGGING_CATEGORY(name, categoryStr)
struct __mavlink_command_long_t mavlink_command_long_t
static MissionCommandTree * instance()
QString rawName(MAV_CMD command) const
Returns the raw name for the specified command.
Simulates MAVLink Gimbal Manager Protocol for MockLink.
DeltaYawMode
Controls how GIMBAL_DEVICE_ATTITUDE_STATUS.delta_yaw and the yaw frame flags are reported.
@ Legacy
No YAW_IN_*_FRAME flag, delta_yaw = 0 (pre-extension firmware wire image)
@ Unknown
Frame flag set, delta_yaw = NaN.
@ Explicit
Frame flag set, delta_yaw = value from setDeltaYawDeg()
void sendGimbalDeviceAttitudeStatusNow()
Sends GIMBAL_DEVICE_ATTITUDE_STATUS immediately instead of waiting for the 1Hz worker.
SetAttitudeCommand lastSetAttitudeCommand() const
QList< int > statusIntervalRequests() const
Every GIMBAL_MANAGER_STATUS interval value (us) received, in order.
bool yawLock() const
void setDeltaYawDeg(float deltaYawDeg)
void setInformationResponse(InformationResponse response)
void sendGimbalManagerStatusWithDeviceId(uint8_t deviceIdField)
void run1HzTasks()
Send periodic gimbal status messages (call from 1Hz tasks)
void setPrimaryControl(uint8_t sysid, uint8_t compid)
Overrides who holds primary control as reported in GIMBAL_MANAGER_STATUS.
bool handleMavlinkMessage(const mavlink_message_t &msg)
void sendGimbalManagerInformationWithDeviceId(uint8_t deviceIdField)
Raw sends with protocol-violating field overrides, for exercising QGC's rejection paths.
void setDeltaYawMode(DeltaYawMode mode)
void setAttitudeDeg(float rollDeg, float pitchDeg, float yawDeg)
Pins gimbal attitude (degrees) and disables the automatic oscillation.
void setYawInEarthFrame(bool earthFrame)
void sendGimbalManagerStatusNow()
void setIgnoreStatusIntervalRequests(int count)
The first count MAV_CMD_SET_MESSAGE_INTERVAL requests for GIMBAL_MANAGER_STATUS are ACKed but not hon...
PitchYawCommand lastPitchYawCommand() const
InformationResponse
How the manager answers MAV_CMD_REQUEST_MESSAGE(GIMBAL_MANAGER_INFORMATION)
@ Silent
Swallow the request, no ACK, no message.
@ Nack
ACK with MAV_RESULT_UNSUPPORTED, no message.
@ Accept
ACK and send the message.
void sendGimbalDeviceAttitudeStatusFrom(uint8_t sourceCompid, uint8_t deviceIdField)
GPSCommandSequence::Command command(QByteArrayView body, GPSTextMatcher reply, std::chrono::milliseconds timeout)
body labelled by its text, framed as a sentence, and completed by reply within timeout.
Last MAV_CMD_DO_GIMBAL_MANAGER_PITCHYAW received (angles in degrees, rates in deg/s)
Last GIMBAL_MANAGER_SET_ATTITUDE received (rates in deg/s)