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
10QGC_LOGGING_CATEGORY(MockLinkGimbalLog, "Comms.MockLink.MockLinkGimbal")
11
13 MockLink *mockLink,
14 bool hasRollAxis,
15 bool hasPitchAxis,
16 bool hasYawAxis,
17 bool hasYawFollow,
18 bool hasYawLock,
19 bool hasRetract,
20 bool hasNeutral)
21 : _mockLink(mockLink)
22 , _hasRollAxis(hasRollAxis)
23 , _hasPitchAxis(hasPitchAxis)
24 , _hasYawAxis(hasYawAxis)
25 , _hasYawFollow(hasYawFollow)
26 , _hasYawLock(hasYawLock)
27 , _hasRetract(hasRetract)
28 , _hasNeutral(hasNeutral)
29{
30}
31
33{
34 // Runs every 1s (1Hz on worker thread). Reads intervals and gimbal state main thread modifies.
35 // Must serialize access to prevent:
36 // - Auto-movement overwriting manual control commands
37 // - Reading stale/inconsistent interval values for status transmission
38 QMutexLocker locker(&_stateMutex);
39 const qint64 nowMs = QDateTime::currentMSecsSinceEpoch();
40
41 // Send GIMBAL_MANAGER_STATUS if interval is set
42 if (_managerStatusIntervalUs > 0) {
43 const qint64 intervalMs = _managerStatusIntervalUs / 1000;
44 if ((nowMs - _managerStatusLastSentMs) >= intervalMs) {
45 _sendGimbalManagerStatus();
46 _managerStatusLastSentMs = nowMs;
47 }
48 }
49
50 // Send GIMBAL_DEVICE_ATTITUDE_STATUS if interval is set
51 if (_deviceAttitudeStatusIntervalUs > 0) {
52 const qint64 intervalMs = _deviceAttitudeStatusIntervalUs / 1000;
53 if ((nowMs - _deviceAttitudeStatusLastSentMs) >= intervalMs) {
54 // Only simulate automatic movement if not under manual control
55 if (!_manualControl) {
56 // Simulate simple gimbal movement (slow sine wave)
57 const qint64 secondsSinceEpoch = nowMs / 1000;
58 _pitch = 10.0f * qSin(secondsSinceEpoch * 0.1); // ±10° pitch oscillation
59 _yaw = 15.0f * qCos(secondsSinceEpoch * 0.15); // ±15° yaw oscillation
60 }
61
62 _sendGimbalDeviceAttitudeStatus();
63 _deviceAttitudeStatusLastSentMs = nowMs;
64 }
65 }
66}
67
69{
70 if (msg.msgid != MAVLINK_MSG_ID_COMMAND_LONG) {
71 return false;
72 }
73
74 mavlink_command_long_t request{};
75 mavlink_msg_command_long_decode(&msg, &request);
76
77 // Handle gimbal-specific commands
78 switch (request.command) {
79 case MAV_CMD_SET_MESSAGE_INTERVAL:
80 if (_handleSetMessageInterval(request)) {
81 _sendCommandAck(request.command, MAV_RESULT_ACCEPTED);
82 return true;
83 }
84 return false;
85
86 case MAV_CMD_REQUEST_MESSAGE:
87 if (_handleRequestMessage(request)) {
88 _sendCommandAck(request.command, MAV_RESULT_ACCEPTED);
89 return true;
90 }
91 return false;
92
93 case MAV_CMD_DO_GIMBAL_MANAGER_PITCHYAW:
94 if (_handleGimbalManagerPitchYaw(request)) {
95 _sendCommandAck(request.command, MAV_RESULT_ACCEPTED);
96 return true;
97 }
98 _sendCommandAck(request.command, MAV_RESULT_DENIED);
99 return true;
100
101 case MAV_CMD_DO_GIMBAL_MANAGER_CONFIGURE:
102 if (_handleGimbalManagerConfigure(request)) {
103 _sendCommandAck(request.command, MAV_RESULT_ACCEPTED);
104 return true;
105 }
106 _sendCommandAck(request.command, MAV_RESULT_DENIED);
107 return true;
108
109 default:
110 return false;
111 }
112}
113
114void MockLinkGimbal::_sendCommandAck(uint16_t command, uint8_t result)
115{
116 QString commandName = MissionCommandTree::instance()->rawName(static_cast<MAV_CMD>(command));
117 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));
118
119 mavlink_message_t msg{};
120 (void) mavlink_msg_command_ack_pack_chan(
121 _mockLink->vehicleId(),
122 MAV_COMP_ID_AUTOPILOT1,
123 _mockLink->mavlinkChannel(),
124 &msg,
125 command,
126 result,
127 0, // progress
128 0, // result_param2
129 0, // target_system
130 0 // target_component
131 );
132 _mockLink->respondWithMavlinkMessage(msg);
133}
134
135bool MockLinkGimbal::_handleSetMessageInterval(const mavlink_command_long_t &request)
136{
137 const int msgId = static_cast<int>(request.param1);
138 const int intervalUs = static_cast<int>(request.param2);
139 const mavlink_message_info_t* info = mavlink_get_message_info_by_id(static_cast<uint32_t>(msgId));
140 QString msgName = info ? info->name : QString::number(msgId);
141
142 qCDebug(MockLinkGimbalLog) << "SET_MESSAGE_INTERVAL -" << QString("%1(%2)").arg(msgName).arg(msgId) << "intervalUs:" << intervalUs;
143
144 // Handle interval values per MAVLink spec:
145 // -1 = disable
146 // 0 = default rate
147 // >0 = interval in microseconds
148 int effectiveInterval = intervalUs;
149 if (intervalUs == 0) {
150 effectiveInterval = kDefaultIntervalUs;
151 } else if (intervalUs < -1) {
152 // Invalid interval
153 return false;
154 }
155
156 // Thread-safe access: Main thread writing interval while worker thread reads every 1s.
157 // Serialize to avoid worker using stale interval for message transmission logic.
158 QMutexLocker locker(&_stateMutex);
159 if (msgId == MAVLINK_MSG_ID_GIMBAL_MANAGER_STATUS) {
160 _managerStatusIntervalUs = effectiveInterval;
161 qCDebug(MockLinkGimbalLog) << msgName << "interval set to" << effectiveInterval << "us";
162 return true;
163 } else if (msgId == MAVLINK_MSG_ID_GIMBAL_DEVICE_ATTITUDE_STATUS) {
164 _deviceAttitudeStatusIntervalUs = effectiveInterval;
165 qCDebug(MockLinkGimbalLog) << msgName << "interval set to" << effectiveInterval << "us";
166 return true;
167 }
168 return false;
169}
170
171bool MockLinkGimbal::_handleRequestMessage(const mavlink_command_long_t &request)
172{
173 const int msgId = static_cast<int>(request.param1);
174 const mavlink_message_info_t* info = mavlink_get_message_info_by_id(static_cast<uint32_t>(msgId));
175 QString msgName = info ? info->name : QString::number(msgId);
176
177 if (msgId == MAVLINK_MSG_ID_GIMBAL_MANAGER_INFORMATION) {
178 qCDebug(MockLinkGimbalLog) << "REQUEST_MESSAGE -" << QString("%1(%2)").arg(msgName).arg(msgId);
179 _sendGimbalManagerInformation();
180 return true;
181 }
182
183 return false;
184}
185
186bool MockLinkGimbal::_handleGimbalManagerPitchYaw(const mavlink_command_long_t &request)
187{
188 // param1: pitch angle (degrees, positive: down, negative: up)
189 // param2: yaw angle (degrees, positive: to the right, negative: to the left)
190 // param3: pitch rate (deg/s)
191 // param4: yaw rate (deg/s)
192 // param5: flags (GIMBAL_MANAGER_FLAGS)
193 // param7: gimbal device id
194
195 const float requestedPitch = request.param1;
196 const float requestedYaw = request.param2;
197 const uint32_t flags = static_cast<uint32_t>(request.param5);
198
199 qCDebug(MockLinkGimbalLog) << "DO_GIMBAL_MANAGER_PITCHYAW - pitch:" << requestedPitch << "yaw:" << requestedYaw << "flags:" << flags;
200
201 // Check if this is a valid request (NaN means no change requested)
202 const bool updatePitch = !qIsNaN(requestedPitch);
203 const bool updateYaw = !qIsNaN(requestedYaw);
204
205 if (!updatePitch && !updateYaw) {
206 qCDebug(MockLinkGimbalLog) << "DO_GIMBAL_MANAGER_PITCHYAW - no valid pitch/yaw requested (both NaN)";
207 return false; // Nothing to do
208 }
209
210 // Thread-safe access: Main thread setting manual control state that worker reads every 1s.
211 // Without lock: Worker could read _manualControl=false and auto-update pitch/yaw after main
212 // sets them to manual values, overwriting the manual control with auto-movement.
213 QMutexLocker locker(&_stateMutex);
214 // Apply limits based on gimbal capabilities
215 if (updatePitch && _hasPitchAxis) {
216 _pitch = qBound(-45.0f, requestedPitch, 45.0f);
217 _manualControl = true; // Switch to manual control mode
218 }
219
220 if (updateYaw && _hasYawAxis) {
221 // Handle yaw lock/follow modes
222 if (flags & GIMBAL_MANAGER_FLAGS_YAW_LOCK) {
223 // Yaw lock mode - maintain absolute yaw
224 _yaw = qBound(-180.0f, requestedYaw, 180.0f);
225 } else {
226 // Follow mode - yaw relative to vehicle
227 _yaw = qBound(-180.0f, requestedYaw, 180.0f);
228 }
229 _manualControl = true; // Switch to manual control mode
230 }
231
232 qCDebug(MockLinkGimbalLog) << "Gimbal commanded to pitch:" << _pitch << "yaw:" << _yaw << "- switched to manual control";
233 return true;
234}
235
236bool MockLinkGimbal::_handleGimbalManagerConfigure(const mavlink_command_long_t &request)
237{
238 // param1: sysid primary control (0: no change)
239 // param2: compid primary control (0: no change)
240 // param3: sysid secondary control (0: no change)
241 // param4: compid secondary control (0: no change)
242 // param7: gimbal device id
243
244 const uint8_t sysidPrimary = static_cast<uint8_t>(request.param1);
245 const uint8_t compidPrimary = static_cast<uint8_t>(request.param2);
246 const uint8_t sysidSecondary = static_cast<uint8_t>(request.param3);
247 const uint8_t compidSecondary = static_cast<uint8_t>(request.param4);
248
249 if (sysidPrimary > 0) {
250 _gimbalManagerSysidPrimary = sysidPrimary;
251 }
252 if (compidPrimary > 0) {
253 _gimbalManagerCompidPrimary = compidPrimary;
254 }
255 if (sysidSecondary > 0) {
256 _gimbalManagerSysidSecondary = sysidSecondary;
257 }
258 if (compidSecondary > 0) {
259 _gimbalManagerCompidSecondary = compidSecondary;
260 }
261
262 qCDebug(MockLinkGimbalLog) << "Gimbal manager configured - Primary:" << _gimbalManagerSysidPrimary
263 << "/" << _gimbalManagerCompidPrimary
264 << "Secondary:" << _gimbalManagerSysidSecondary
265 << "/" << _gimbalManagerCompidSecondary;
266 return true;
267}
268
269void MockLinkGimbal::_sendGimbalManagerStatus()
270{
271 qCDebug(MockLinkGimbalLog) << "Sending GIMBAL_MANAGER_STATUS";
272
273 mavlink_message_t msg{};
274 (void) mavlink_msg_gimbal_manager_status_pack_chan(
275 _mockLink->vehicleId(),
276 MAV_COMP_ID_AUTOPILOT1,
277 _mockLink->mavlinkChannel(),
278 &msg,
279 0, // time_boot_ms
280 0, // flags
281 kGimbalCompId, // gimbal_device_id
282 _gimbalManagerSysidPrimary,
283 _gimbalManagerCompidPrimary,
284 _gimbalManagerSysidSecondary,
285 _gimbalManagerCompidSecondary);
286 _mockLink->respondWithMavlinkMessage(msg);
287}
288
289void MockLinkGimbal::_sendGimbalDeviceAttitudeStatus()
290{
291 qCDebug(MockLinkGimbalLog) << "Sending GIMBAL_DEVICE_ATTITUDE_STATUS - pitch:" << _pitch << "yaw:" << _yaw << "manual:" << _manualControl;
292
293 // Convert Euler angles (degrees) to quaternion
294 const float rollRad = qDegreesToRadians(_roll);
295 const float pitchRad = qDegreesToRadians(_pitch);
296 const float yawRad = qDegreesToRadians(_yaw);
297
298 const float cy = qCos(yawRad * 0.5f);
299 const float sy = qSin(yawRad * 0.5f);
300 const float cp = qCos(pitchRad * 0.5f);
301 const float sp = qSin(pitchRad * 0.5f);
302 const float cr = qCos(rollRad * 0.5f);
303 const float sr = qSin(rollRad * 0.5f);
304
305 float q[4];
306 q[0] = cr * cp * cy + sr * sp * sy; // w
307 q[1] = sr * cp * cy - cr * sp * sy; // x
308 q[2] = cr * sp * cy + sr * cp * sy; // y
309 q[3] = cr * cp * sy - sr * sp * cy; // z
310
311 mavlink_message_t msg{};
312 (void) mavlink_msg_gimbal_device_attitude_status_pack_chan(
313 _mockLink->vehicleId(),
314 kGimbalCompId,
315 _mockLink->mavlinkChannel(),
316 &msg,
317 0, 0, // target system, component
318 0, // time_boot_ms
319 GIMBAL_DEVICE_FLAGS_NEUTRAL | GIMBAL_DEVICE_FLAGS_ROLL_LOCK | GIMBAL_DEVICE_FLAGS_PITCH_LOCK,
320 q,
321 0.0f, 0.0f, 0.0f, // angular_velocity_x, y, z
322 0, // failure flags
323 _pitch, _yaw, // delta_pitch, delta_yaw (Euler angles in degrees)
324 0); // gimbal_device_id = 0 means the device id is the same as the message component ID
325 _mockLink->respondWithMavlinkMessage(msg);
326}
327
328void MockLinkGimbal::_sendGimbalManagerInformation()
329{
330 // Build capability flags bitmask based on configuration
331 uint32_t capFlags = 0;
332 if (_hasRollAxis) capFlags |= GIMBAL_MANAGER_CAP_FLAGS_HAS_ROLL_AXIS;
333 if (_hasPitchAxis) capFlags |= GIMBAL_MANAGER_CAP_FLAGS_HAS_PITCH_AXIS;
334 if (_hasYawAxis) capFlags |= GIMBAL_MANAGER_CAP_FLAGS_HAS_YAW_AXIS;
335 if (_hasYawFollow) capFlags |= GIMBAL_MANAGER_CAP_FLAGS_HAS_YAW_FOLLOW;
336 if (_hasYawLock) capFlags |= GIMBAL_MANAGER_CAP_FLAGS_HAS_YAW_LOCK;
337 if (_hasRetract) capFlags |= GIMBAL_MANAGER_CAP_FLAGS_HAS_RETRACT;
338 if (_hasNeutral) capFlags |= GIMBAL_MANAGER_CAP_FLAGS_HAS_NEUTRAL;
339
340 qCDebug(MockLinkGimbalLog) << "Sending GIMBAL_MANAGER_INFORMATION - capFlags:" << QString::number(capFlags, 16);
341
342 mavlink_message_t msg{};
343 (void) mavlink_msg_gimbal_manager_information_pack_chan(
344 _mockLink->vehicleId(),
345 MAV_COMP_ID_AUTOPILOT1,
346 _mockLink->mavlinkChannel(),
347 &msg,
348 0, // time_boot_ms
349 capFlags,
350 kGimbalCompId,
351 -45, 45,
352 -45, 45,
353 -180, 180);
354 _mockLink->respondWithMavlinkMessage(msg);
355}
struct __mavlink_message mavlink_message_t
#define QGC_LOGGING_CATEGORY(name, categoryStr)
struct __mavlink_command_long_t mavlink_command_long_t
uint8_t mavlinkChannel() const
static MissionCommandTree * instance()
QString rawName(MAV_CMD command) const
Returns the raw name for the specified command.
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)