QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
RemoteIDManager.cc
Go to the documentation of this file.
1#include "RemoteIDManager.h"
2#include "MAVLinkLib.h"
3#include "SettingsManager.h"
4#include "RemoteIDSettings.h"
5#include "PositionManager.h"
6#include "Vehicle.h"
8#include "MAVLinkProtocol.h"
10
11QGC_LOGGING_CATEGORY(RemoteIDManagerLog, "Vehicle.RemoteIDManager")
12
13#define AREA_COUNT 1
14#define AREA_RADIUS 0
15#define MAVLINK_UNKNOWN_METERS -1000.0f
16#define MAVLINK_UNKNOWN_LAT 0
17#define MAVLINK_UNKNOWN_LON 0
18#define SENDING_RATE_MSEC 1000
19#define ALLOWED_GPS_DELAY 5000
20#define RID_TIMEOUT 2500 // Messages should be arriving at 1 Hz, so we set a 2 second timeout
21
22const uint8_t* RemoteIDManager::_id_or_mac_unknown = new uint8_t[MAVLINK_MSG_OPEN_DRONE_ID_OPERATOR_ID_FIELD_ID_OR_MAC_LEN]();
23
25 : QObject (vehicle)
26 , _vehicle (vehicle)
27 , _settings (nullptr)
28 , _armStatusGood (false)
29 , _commsGood (false)
30 , _gcsGPSGood (false)
31 , _basicIDGood (true)
32 , _GCSBasicIDValid (false)
33 , _operatorIDGood (false)
34 , _emergencyDeclared (false)
35 , _targetSystem (0) // By default 0 means broadcast
36 , _targetComponent (0) // By default 0 means broadcast
37 , _enforceSendingSelfID (false)
38{
40
41 // Timer to track a healthy RID device. When expired we let the operator know
42 _odidTimeoutTimer.setSingleShot(true);
43 _odidTimeoutTimer.setInterval(RID_TIMEOUT);
44 connect(&_odidTimeoutTimer, &QTimer::timeout, this, &RemoteIDManager::_odidTimeout);
45
46 // Timer to send messages at a constant rate
47 _sendMessagesTimer.setInterval(SENDING_RATE_MSEC);
48 connect(&_sendMessagesTimer, &QTimer::timeout, this, &RemoteIDManager::_sendMessages);
49
50 // GCS GPS position updates to track the health of the GPS data
51 connect(QGCPositionManager::instance(), &QGCPositionManager::positionInfoUpdated, this, &RemoteIDManager::_updateLastGCSPositionInfo);
52
53 // Check changes in basic id settings as long as they are modified
54 connect(_settings->basicID(), &Fact::rawValueChanged, this, &RemoteIDManager::_checkGCSBasicID);
55 connect(_settings->basicIDType(), &Fact::rawValueChanged, this, &RemoteIDManager::_checkGCSBasicID);
56 connect(_settings->basicIDUaType(), &Fact::rawValueChanged, this, &RemoteIDManager::_checkGCSBasicID);
57
58 // Assign vehicle sysid and compid. GCS must target these messages to autopilot, and autopilot will redirect them to RID device
59 _targetSystem = _vehicle->id();
60 _targetComponent = _vehicle->compId();
61
62 if (_settings->operatorIDValid()->rawValue() == true || (_settings->region()->rawValue().toInt() != Region::EU && _settings->operatorID()->rawValue().toString().length() > 0)) {
63 // If it was already checked, we can flag this as good to go.
64 // We don't do a fresh verification because we don't store the private part of the ID.
65 _operatorIDGood = true;
67 }
68}
69
71{
72 switch (message.msgid) {
73 // So far we are only listening to this one, as heartbeat won't be sent if connected by CAN
74 case MAVLINK_MSG_ID_OPEN_DRONE_ID_ARM_STATUS:
75 _handleArmStatus(message);
76 default:
77 break;
78 }
79}
80
81// This slot will be called if we stop receiving heartbeats for more than RID_TIMEOUT seconds
82void RemoteIDManager::_odidTimeout()
83{
84 _commsGood = false;
85 _sendMessagesTimer.stop(); // We stop sending messages if the communication with the RID device is down
86 emit commsGoodChanged();
87 qCDebug(RemoteIDManagerLog) << "We stopped receiving heartbeat from RID device.";
88}
89
90// Parsing of the ARM_STATUS message comming from the RID device
91void RemoteIDManager::_handleArmStatus(mavlink_message_t& message)
92{
93 // Compid must be ODID_TXRX_X
94 if ( (message.compid < MAV_COMP_ID_ODID_TXRX_1) || (message.compid > MAV_COMP_ID_ODID_TXRX_3) ) {
95 // or same as autopilot, in the case of Ardupilot and CAN RID modules
96 if (message.compid != MAV_COMP_ID_AUTOPILOT1) {
97 return;
98 }
99 }
100
101 // Sanity check, only get messages from same sysid
102 if (_vehicle->id() != message.sysid) {
103 return;
104 }
105
106 if (!_available) {
107 _available = true;
108 emit availableChanged();
109 qCDebug(RemoteIDManagerLog) << "Receiving ODID_ARM_STATUS for first time. Mavlink Open Drone ID support is available.";
110 }
111
112 // We set the targetsystem
113 if (_targetSystem != message.sysid) {
114 _targetSystem = message.sysid;
115 qCDebug(RemoteIDManagerLog) << "Subscribing to ODID messages coming from system " << _targetSystem;
116 }
117
118 if (!_commsGood) {
119 _commsGood = true;
120 _sendMessagesTimer.start(); // Start sending our messages
121 _checkGCSBasicID(); // Check if basicID is good to send
122 emit commsGoodChanged();
123 qCDebug(RemoteIDManagerLog) << "Receiving ODID_ARM_STATUS from RID device";
124 }
125
126 // Restart the timeout
127 _odidTimeoutTimer.start();
128
129 // CompId and sysId are correct, we can proceed
130 mavlink_open_drone_id_arm_status_t armStatus;
131 mavlink_msg_open_drone_id_arm_status_decode(&message, &armStatus);
132
133 if (armStatus.status == MAV_ODID_ARM_STATUS_GOOD_TO_ARM && !_armStatusGood) {
134 // If good to arm, even if basic ID is not set on GCS, it was set by remoteID parameters, so GCS one would be optional in this case
135 if (!_basicIDGood) {
136 _basicIDGood = true;
137 emit basicIDGoodChanged();
138 }
139 _armStatusGood = true;
141 qCDebug(RemoteIDManagerLog) << "Arm status GOOD TO ARM.";
142 }
143
144 if (armStatus.status == MAV_ODID_ARM_STATUS_PRE_ARM_FAIL_GENERIC) {
145 _armStatusGood = false;
146 _armStatusError = QString::fromLocal8Bit(armStatus.error);
147 // Check if the error is because of missing basic id
148 if (armStatus.error == QString("missing basic_id message")) {
149 _basicIDGood = false;
150 qCDebug(RemoteIDManagerLog) << "Arm status error, basic_id is not set in RID device nor in GCS!";
151 emit basicIDGoodChanged();
152 }
155 qCDebug(RemoteIDManagerLog) << "Arm status error:" << _armStatusError;
156 }
157}
158
159// Function that sends messages periodically
160void RemoteIDManager::_sendMessages()
161{
162 // We always try to send System
163 _sendSystem();
164
165 // only send it if the information is correct and the tickbox in settings is set
166 if (_GCSBasicIDValid && _settings->sendBasicID()->rawValue().toBool()) {
167 _sendBasicID();
168 }
169
170 // We only send selfID if the pilot wants it or in case of a declared emergency. If an emergency is cleared
171 // we also keep sending the message, to be sure the non emergency state makes it up to the vehicle
172 if (_settings->sendSelfID()->rawValue().toBool() || _emergencyDeclared || _enforceSendingSelfID) {
173 _sendSelfIDMsg();
174 }
175
176 // We only send the OperatorID if the pilot wants it or if the region we have set is europe.
177 // To be able to send it, it needs to be filled correclty
178 if ((_settings->sendOperatorID()->rawValue().toBool() || (_settings->region()->rawValue().toInt() == Region::EU)) && _operatorIDGood) {
179 _sendOperatorID();
180 }
181
182}
183
184void RemoteIDManager::_sendSelfIDMsg()
185{
186 WeakLinkInterfacePtr weakLink = _vehicle->vehicleLinkManager()->primaryLink();
187 SharedLinkInterfacePtr sharedLink = weakLink.lock();
188
189 if (sharedLink) {
191 const QByteArray selfIdDescription = _getSelfIDDescription();
192
193 mavlink_msg_open_drone_id_self_id_pack_chan(MAVLinkProtocol::instance()->getSystemId(),
195 sharedLink->mavlinkChannel(),
196 &msg,
197 _targetSystem,
198 _targetComponent,
199 _id_or_mac_unknown,
200 _emergencyDeclared ? 1 : _settings->selfIDType()->rawValue().toInt(), // If emergency is delcared we send directly a 1 (1 = EMERGENCY)
201 selfIdDescription.constData()); // Depending on the type of SelfID we send a different description
202 _vehicle->sendMessageOnLinkThreadSafe(sharedLink.get(), msg);
203 }
204}
205
206// We need to return the correct description for the self ID type we have selected
207QByteArray RemoteIDManager::_getSelfIDDescription() const
208{
209 QString descriptionToSend;
210
211 if (_emergencyDeclared) {
212 // If emergency is declared we dont care about the settings and we send emergency directly
213 descriptionToSend = _settings->selfIDEmergency()->rawValue().toString();
214 } else {
215 switch (_settings->selfIDType()->rawValue().toInt()) {
216 case 0:
217 descriptionToSend = _settings->selfIDFree()->rawValue().toString();
218 break;
219 case 1:
220 descriptionToSend = _settings->selfIDEmergency()->rawValue().toString();
221 break;
222 case 2:
223 descriptionToSend = _settings->selfIDExtended()->rawValue().toString();
224 break;
225 default:
226 descriptionToSend = _settings->selfIDEmergency()->rawValue().toString();
227 }
228 }
229
230 QByteArray descriptionBuffer = descriptionToSend.toLocal8Bit();
231 descriptionBuffer.resize(MAVLINK_MSG_OPEN_DRONE_ID_SELF_ID_FIELD_DESCRIPTION_LEN);
232 return descriptionBuffer;
233}
234
235void RemoteIDManager::_sendOperatorID()
236{
237 WeakLinkInterfacePtr weakLink = _vehicle->vehicleLinkManager()->primaryLink();
238 SharedLinkInterfacePtr sharedLink = weakLink.lock();
239
240 if (sharedLink) {
242
243 QByteArray bytesOperatorID = (_settings->operatorID()->rawValue().toString()).toLocal8Bit();
244 bytesOperatorID.resize(MAVLINK_MSG_OPEN_DRONE_ID_OPERATOR_ID_FIELD_OPERATOR_ID_LEN);
245
246 mavlink_msg_open_drone_id_operator_id_pack_chan(
247 MAVLinkProtocol::instance()->getSystemId(),
249 sharedLink->mavlinkChannel(),
250 &msg,
251 _targetSystem,
252 _targetComponent,
253 _id_or_mac_unknown,
254 _settings->operatorIDType()->rawValue().toInt(),
255 bytesOperatorID.constData());
256
257 _vehicle->sendMessageOnLinkThreadSafe(sharedLink.get(), msg);
258 }
259}
260
261void RemoteIDManager::_updateGcsGpsStatus(bool gpsGood, const QString& error)
262{
263 if (!error.isEmpty() && _gcsGPSError != error) {
264 _gcsGPSError = error;
265 qCWarning(RemoteIDManagerLog) << "GCS GPS error:" << error;
266 }
267 if (_gcsGPSGood != gpsGood) {
268 _gcsGPSGood = gpsGood;
269 if (gpsGood) {
270 _gcsGPSError.clear();
271 }
272 emit gcsGPSGoodChanged();
273 }
274}
275
276void RemoteIDManager::_sendSystem()
277{
278 QGeoCoordinate gcsPosition(0, 0, 0);
279 const uint32_t locationType = _settings->locationType()->rawValue().toUInt();
280 // Location types:
281 // 0 -> TAKEOFF (not supported yet)
282 // 1 -> LIVE GNNS
283 // 2 -> FIXED
284 if (locationType == LocationTypes::FIXED) {
285 const double lat = _settings->latitudeFixed()->rawValue().toDouble();
286 const double lon = _settings->longitudeFixed()->rawValue().toDouble();
287 const double alt = _settings->altitudeFixed()->rawValue().toDouble();
288
289 // For FIXED location, we first check that the values are valid. Then we populate our position
290 if (lat >= -90.0 && lat <= 90.0 && lon >= -180.0 && lon <= 180.0) {
291 gcsPosition = QGeoCoordinate(lat, lon, alt);
292 _updateGcsGpsStatus(true);
293 } else {
294 _updateGcsGpsStatus(false, "The provided coordinates for FIXED position are invalid.");
295 }
296 } else {
298 QGeoPositionInfo geoPositionInfo = positionManager->geoPositionInfo();
299 gcsPosition = positionManager->gcsPosition();
300
301 if (!geoPositionInfo.isValid()) {
302 _updateGcsGpsStatus(false, "GCS GPS data is not valid.");
303 } else if (positionManager->gcsPositioningError() != QGeoPositionInfoSource::NoError && positionManager->gcsPositioningError() != QGeoPositionInfoSource::UpdateTimeoutError) {
304 _updateGcsGpsStatus(false, QString("GCS GPS data error: %1").arg(positionManager->gcsPositioningError()));
305 } else if (!gcsPosition.isValid() || gcsPosition.type() == QGeoCoordinate::InvalidCoordinate) {
306 _updateGcsGpsStatus(false, "GCS GPS data error: Invalid coordinate type.");
307 } else if (_settings->region()->rawValue().toInt() == Region::FAA && gcsPosition.type() != QGeoCoordinate::Coordinate3D) {
308 // FAA requires altitude data, or else the GPS data is not good
309 _updateGcsGpsStatus(false, "GCS GPS data error: Altitude data is mandatory for FAA regions.");
310 } else if (_lastGeoPositionTimeStamp.msecsTo(QDateTime::currentDateTime().currentDateTimeUtc()) > ALLOWED_GPS_DELAY) {
311 _updateGcsGpsStatus(false, "GCS GPS data is older than 5 seconds");
312 } else {
313 _updateGcsGpsStatus(true);
314 }
315 }
316
317 WeakLinkInterfacePtr weakLink = _vehicle->vehicleLinkManager()->primaryLink();
318 SharedLinkInterfacePtr sharedLink = weakLink.lock();
319
320 if (sharedLink) {
322
323 mavlink_msg_open_drone_id_system_pack_chan(MAVLinkProtocol::instance()->getSystemId(),
325 sharedLink->mavlinkChannel(),
326 &msg,
327 _targetSystem,
328 _targetComponent,
329 _id_or_mac_unknown,
330 _settings->locationType()->rawValue().toUInt(),
331 _settings->classificationType()->rawValue().toUInt(),
332 _gcsGPSGood ? ( gcsPosition.latitude() * 1.0e7 ) : MAVLINK_UNKNOWN_LAT,
333 _gcsGPSGood ? ( gcsPosition.longitude() * 1.0e7 ) : MAVLINK_UNKNOWN_LON,
338 _settings->categoryEU()->rawValue().toUInt(),
339 _settings->classEU()->rawValue().toUInt(),
340 _gcsGPSGood ? gcsPosition.altitude() : MAVLINK_UNKNOWN_METERS,
341 _timestamp2019()), // Time stamp needs to be since 00:00:00 1/1/2019
342 _vehicle->sendMessageOnLinkThreadSafe(sharedLink.get(), msg);
343 }
344}
345
346// Returns seconds elapsed since 00:00:00 1/1/2019
347uint32_t RemoteIDManager::_timestamp2019()
348{
349 uint32_t secsSinceEpoch2019 = 1546300800; // Secs elapsed since epoch to 1-1-2019
350
351 return ((QDateTime::currentDateTime().currentSecsSinceEpoch()) - secsSinceEpoch2019);
352}
353
354void RemoteIDManager::_sendBasicID()
355{
356 WeakLinkInterfacePtr weakLink = _vehicle->vehicleLinkManager()->primaryLink();
357 SharedLinkInterfacePtr sharedLink = weakLink.lock();
358
359 if (sharedLink) {
361
362 QString basicIDTemp = _settings->basicID()->rawValue().toString();
363 QByteArray ba = basicIDTemp.toLocal8Bit();
364 // To make sure the buffer is large enough to fit the message. It will add padding bytes if smaller, or exclude the extra ones if bigger
365 ba.resize(MAVLINK_MSG_OPEN_DRONE_ID_BASIC_ID_FIELD_UAS_ID_LEN);
366
367 mavlink_msg_open_drone_id_basic_id_pack_chan(MAVLinkProtocol::instance()->getSystemId(),
369 sharedLink->mavlinkChannel(),
370 &msg,
371 _targetSystem,
372 _targetComponent,
373 _id_or_mac_unknown,
374 _settings->basicIDType()->rawValue().toUInt(),
375 _settings->basicIDUaType()->rawValue().toUInt(),
376 reinterpret_cast<const unsigned char*>(ba.constData())),
377
378 _vehicle->sendMessageOnLinkThreadSafe(sharedLink.get(), msg);
379 }
380}
381
382void RemoteIDManager::_checkGCSBasicID()
383{
384 QString basicID = _settings->basicID()->rawValue().toString();
385
386 if (!basicID.isEmpty() && (_settings->basicIDType()->rawValue().toInt() >= 0) && (_settings->basicIDUaType()->rawValue().toInt() >= 0)) {
387 _GCSBasicIDValid = true;
388 } else {
389 _GCSBasicIDValid = false;
390 }
391}
392
393void RemoteIDManager::checkOperatorID(const QString& operatorID)
394{
395 // We overwrite the fact that is also set by the text input but we want to update
396 // after every letter rather than when editing is done.
397 // We check whether it actually changed to avoid triggering this on startup.
398 if (operatorID != _settings->operatorID()->rawValueString()) {
399 _settings->operatorIDValid()->setRawValue(_isEUOperatorIDValid(operatorID));
400 }
401}
402
404{
405 QString operatorID = _settings->operatorID()->rawValue().toString();
406
407 if (_settings->region()->rawValue().toInt() == Region::EU) {
408 // Save for next time because we don't save the private part,
409 // so we can't re-verify next time and just trust the value
410 // in the settings.
411 _operatorIDGood = _settings->operatorIDValid()->rawValue() == true;
412 if (_operatorIDGood) {
413 // Strip private part
414 _settings->operatorID()->setRawValue(operatorID.sliced(0, 16));
415 }
416
417 } else {
418 // Otherwise, we just check if there is anything entered
419 _operatorIDGood =
420 (!operatorID.isEmpty() && (_settings->operatorIDType()->rawValue().toInt() >= 0));
421 }
422
424}
425
426bool RemoteIDManager::_isEUOperatorIDValid(const QString& operatorID) const
427{
428 const bool containsDash = operatorID.contains('-');
429 if (!(operatorID.length() == 20 && containsDash) && !(operatorID.length() == 19 && !containsDash)) {
430 qCDebug(RemoteIDManagerLog) << "OperatorID not long enough";
431 return false;
432 }
433
434 const QString countryCode = operatorID.sliced(0,3);
435 if (!countryCode.isUpper()) {
436 qCDebug(RemoteIDManagerLog) << "OperatorID country code not uppercase";
437 return false;
438 }
439
440 const QString number = operatorID.sliced(3, 12);
441 const QChar checksum = operatorID.at(15);
442 const QString secret = containsDash ? operatorID.sliced(17, 3) : operatorID.sliced(16, 3);
443 const QString combination = number + secret;
444
445 const QChar result = _calculateLuhnMod36(combination);
446
447 const bool valid = (result == checksum);
448 qCDebug(RemoteIDManagerLog) << "Operator ID checksum " << (valid ? "valid" : "invalid");
449 return valid;
450}
451
452QChar RemoteIDManager::_calculateLuhnMod36(const QString& input) const {
453 const int n = 36;
454 const QString alphabet = "0123456789abcdefghijklmnopqrstuvwxyz";
455
456 int sum = 0;
457 int factor = 2;
458
459 for (int i = input.length() - 1; i >= 0; i--) {
460 int codePoint = alphabet.indexOf(input[i]);
461 int addend = factor * codePoint;
462 factor = (factor == 2) ? 1 : 2;
463 addend = (addend / n) + (addend % n);
464 sum += addend;
465 }
466
467 int remainder = sum % n;
468 int checkCodePoint = (n - remainder) % n;
469 return alphabet.at(checkCodePoint);
470}
471
473{
474 _emergencyDeclared = declare;
476 // Wether we are starting an emergency or cancelling it, we need to enforce sending
477 // this message. Otherwise, if non optimal connection quality, vehicle RID device
478 // could remain in the wrong state. It is clarified to the user in remoteidsettings.qml
479 _enforceSendingSelfID = true;
480
481 qCDebug(RemoteIDManagerLog) << ( declare ? "Emergency declared." : "Emergency cleared.");
482}
483
484void RemoteIDManager::_updateLastGCSPositionInfo(QGeoPositionInfo update)
485{
486 if (update.isValid()) {
487 _lastGeoPositionTimeStamp = update.timestamp().toUTC();
488 }
489}
std::shared_ptr< LinkInterface > SharedLinkInterfacePtr
std::weak_ptr< LinkInterface > WeakLinkInterfacePtr
Error error
struct __mavlink_message mavlink_message_t
#define QGC_LOGGING_CATEGORY(name, categoryStr)
#define MAVLINK_UNKNOWN_LAT
#define SENDING_RATE_MSEC
#define AREA_COUNT
#define MAVLINK_UNKNOWN_METERS
#define ALLOWED_GPS_DELAY
#define AREA_RADIUS
#define RID_TIMEOUT
#define MAVLINK_UNKNOWN_LON
void rawValueChanged(const QVariant &value)
static int getComponentId()
static MAVLinkProtocol * instance()
QGeoPositionInfo geoPositionInfo() const
static QGCPositionManager * instance()
QGeoCoordinate gcsPosition() const
QGeoPositionInfoSource::Error gcsPositioningError() const
void positionInfoUpdated(QGeoPositionInfo update)
void availableChanged()
void mavlinkMessageReceived(mavlink_message_t &message)
void commsGoodChanged()
void operatorIDGoodChanged()
Q_INVOKABLE void setEmergency(bool declare)
void armStatusErrorChanged()
void basicIDGoodChanged()
Q_INVOKABLE void setOperatorID()
void emergencyDeclaredChanged()
void armStatusGoodChanged()
RemoteIDManager(Vehicle *vehicle)
true: the vehicle supports Mavlink Open Drone ID messages
void gcsGPSGoodChanged()
Q_INVOKABLE void checkOperatorID(const QString &operatorID)
static SettingsManager * instance()
RemoteIDSettings * remoteIDSettings() const
WeakLinkInterfacePtr primaryLink() const
VehicleLinkManager * vehicleLinkManager()
Definition Vehicle.h:575
int id() const
Definition Vehicle.h:425
bool sendMessageOnLinkThreadSafe(LinkInterface *link, mavlink_message_t message)
Definition Vehicle.cc:1389
int compId() const
Definition Vehicle.h:426