QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
RemoteIDSettings.cc
Go to the documentation of this file.
1#include "RemoteIDSettings.h"
2
3#include <QtCore/QSettings>
4
6#include "SettingsManager.h"
7
8QGC_LOGGING_CATEGORY(RemoteIDSettingsLog, "Settings.RemoteIDSettings")
9
10DECLARE_SETTINGGROUP(RemoteID, "RemoteID")
11{
12 migrateLegacyOperatorID();
13
14 // Reject UI writes of invalid EU operator IDs so a stored EU ID is always valid by construction
15 operatorIDEU()->metaData()->setCustomCookedValidator(&RemoteIDSettings::_euOperatorIDCookedValidator);
16
17 connect(operatorIDEU(), &Fact::rawValueChanged, this, &RemoteIDSettings::_operatorIDEUChanged);
18 connect(operatorIDFAA(), &Fact::rawValueChanged, this, &RemoteIDSettings::_updateOperatorIDValidForRegion);
19 connect(region(), &Fact::rawValueChanged, this, &RemoteIDSettings::_regionChanged);
20 connect(basicID(), &Fact::rawValueChanged, this, &RemoteIDSettings::_updateBasicIDValidity);
21 connect(basicIDType(), &Fact::rawValueChanged, this, &RemoteIDSettings::_updateBasicIDValidity);
22 connect(basicIDUaType(), &Fact::rawValueChanged, this, &RemoteIDSettings::_updateBasicIDValidity);
23
24 _updateOperatorIDValidForRegion();
25 _updateBasicIDValidity();
26}
27
29{
30 QSettings qSettings;
31 qSettings.beginGroup(settingsGroup);
32
33 // Chain of legacy keys: operatorIDValid (shipped v4.4.0 - v5.0.x) was renamed to
34 // operatorIDEUValid (never in a release), which was then replaced (along with the single
35 // operatorID fact, shipped through v5.0.x) by the per-region facts.
36 const QString legacyOperatorIDKey = QStringLiteral("operatorID");
37 const QString legacyValidKey = QStringLiteral("operatorIDValid");
38 const QString legacyEUValidKey = QStringLiteral("operatorIDEUValid");
39
40 if (qSettings.contains(legacyOperatorIDKey)) {
41 const QString legacyOperatorID = qSettings.value(legacyOperatorIDKey).toString();
42 const bool legacyEUValid = qSettings.value(legacyEUValidKey, qSettings.value(legacyValidKey, false)).toBool();
43
44 if (!legacyOperatorID.isEmpty()) {
45 if (legacyEUValid) {
46 // Keep only the 16-char public part: legacy stores could hold the full ID
47 // including the secret chars, and the sanitize handler isn't connected yet
48 operatorIDEU()->setRawValue(legacyOperatorID.left(16));
49 } else {
50 operatorIDFAA()->setRawValue(legacyOperatorID);
51 }
52 }
53 qSettings.remove(legacyOperatorIDKey);
54 }
55 qSettings.remove(legacyValidKey);
56 qSettings.remove(legacyEUValidKey);
57
58 qSettings.endGroup();
59}
60
61QString RemoteIDSettings::_euOperatorIDCookedValidator(const QVariant& cookedValue)
62{
64 const QString candidate = cookedValue.toString();
65
66 // Clearing and no-op re-commits of the stored (sanitized) value are always legal
67 if (candidate.isEmpty() || (candidate == settings->operatorIDEU()->rawValue().toString())) {
68 return QString();
69 }
70
71 if (_isEUOperatorIDValid(candidate)) {
72 return QString();
73 }
74 return tr("Invalid Operator ID. Enter the full 19 or 20 character ID including the 3 secret characters.");
75}
76
77void RemoteIDSettings::_operatorIDEUChanged()
78{
79 if (_updatingOperatorID) {
80 return;
81 }
82
83 // A full EU operator ID passed validation on write. Keep only the public 16-character part:
84 // the 3 secret characters must not be stored or broadcast.
85 const QString currentOperatorID = operatorIDEU()->rawValue().toString();
86 if ((currentOperatorID.length() > 16) && _isEUOperatorIDValid(currentOperatorID)) {
87 _updatingOperatorID = true;
88 operatorIDEU()->setRawValue(currentOperatorID.sliced(0, 16));
89 _updatingOperatorID = false;
90 }
91
92 _updateOperatorIDValidForRegion();
93}
94
95void RemoteIDSettings::_regionChanged()
96{
97 const int newRegion = region()->rawValue().toInt();
98
99 if (newRegion == static_cast<int>(RegionOperation::EU)) {
100 // EU regulation requires broadcasting the operator ID
101 sendOperatorID()->setRawValue(true);
102 } else if (newRegion == static_cast<int>(RegionOperation::FAA)) {
103 // FAA requires the live operator position, not a fixed location
104 locationType()->setRawValue(static_cast<int>(LocationType::LIVE));
105 }
106
107 _updateOperatorIDValidForRegion();
108}
109
110void RemoteIDSettings::_updateOperatorIDValidForRegion()
111{
112 const bool isEURegion = (region()->rawValue().toInt() == static_cast<int>(RegionOperation::EU));
113 const QString currentOperatorID = (isEURegion ? operatorIDEU() : operatorIDFAA())->rawValue().toString();
114
115 // EU IDs are valid by construction (write-gated + sanitized), FAA IDs have no published
116 // spec to validate against, so presence is the only requirement for either region.
117 const bool validForRegion = !currentOperatorID.isEmpty();
118
119 if (_operatorIDValidForRegion != validForRegion) {
120 _operatorIDValidForRegion = validForRegion;
122 }
123}
124
125void RemoteIDSettings::_updateBasicIDValidity()
126{
127 // Type facts are uint8 enums, so any value is structurally valid: presence of the ID is the only requirement
128 const bool valid = !basicID()->rawValue().toString().isEmpty();
129
130 if (_basicIDValid != valid) {
131 _basicIDValid = valid;
132 emit basicIDValidChanged();
133 }
134}
135
136bool RemoteIDSettings::_isEUOperatorIDValid(const QString& operatorID)
137{
138 // EN 4709-002 format: 3-letter country code + 12 chars [0-9a-z] + 1 checksum char,
139 // optionally followed by '-' and 3 secret chars [0-9a-z]
140 const bool containsDash = operatorID.contains('-');
141 if (!(operatorID.length() == 20 && containsDash) && !(operatorID.length() == 19 && !containsDash)) {
142 qCDebug(RemoteIDSettingsLog) << "OperatorID format mismatch: expected 20 chars with dash or 19 chars without";
143 return false;
144 }
145 if (containsDash && (operatorID.at(16) != '-')) {
146 qCDebug(RemoteIDSettingsLog) << "OperatorID dash separator not at expected position";
147 return false;
148 }
149
150 const QString countryCode = operatorID.sliced(0, 3);
151 for (const QChar& c : countryCode) {
152 // ISO 3166-1 alpha-3 country codes are strictly ASCII A-Z; QChar::isUpper would
153 // also accept Unicode uppercase, which the checksum doesn't cover
154 if (c < QLatin1Char('A') || c > QLatin1Char('Z')) {
155 qCDebug(RemoteIDSettingsLog) << "OperatorID country code must be 3 uppercase ASCII letters";
156 return false;
157 }
158 }
159
160 const QString number = operatorID.sliced(3, 12);
161 const QChar checksum = operatorID.at(15);
162 const QString secret = containsDash ? operatorID.sliced(17, 3) : operatorID.sliced(16, 3);
163 const QString combination = number + secret + checksum;
164
165 const QString alphabet = QStringLiteral("0123456789abcdefghijklmnopqrstuvwxyz");
166 for (const QChar& c : combination) {
167 if (!alphabet.contains(c)) {
168 qCDebug(RemoteIDSettingsLog) << "OperatorID contains characters outside [0-9a-z]";
169 return false;
170 }
171 }
172
173 const QChar result = _calculateLuhnMod36(number + secret);
174
175 const bool valid = (result == checksum);
176 qCDebug(RemoteIDSettingsLog) << "Operator ID checksum" << (valid ? "valid" : "invalid");
177 return valid;
178}
179
180QChar RemoteIDSettings::_calculateLuhnMod36(const QString& input)
181{
182 const int n = 36;
183 const QString alphabet = QStringLiteral("0123456789abcdefghijklmnopqrstuvwxyz");
184
185 int sum = 0;
186 int factor = 2;
187
188 for (int i = input.length() - 1; i >= 0; i--) {
189 const int codePoint = alphabet.indexOf(input.at(i));
190 int addend = factor * codePoint;
191 factor = (factor == 2) ? 1 : 2;
192 addend = (addend / n) + (addend % n);
193 sum += addend;
194 }
195
196 const int remainder = sum % n;
197 const int checkCodePoint = (n - remainder) % n;
198 return alphabet.at(checkCodePoint);
199}
200
219DECLARE_SETTINGSFACT(RemoteIDSettings, classificationType)
#define QGC_LOGGING_CATEGORY(name, categoryStr)
#define DECLARE_SETTINGSFACT(CLASS, NAME)
#define DECLARE_SETTINGGROUP(NAME, GROUP)
void rawValueChanged(const QVariant &value)
void basicIDValidChanged()
void operatorIDValidForRegionChanged()
QString settingsGroup() const
static SettingsManager * instance()
RemoteIDSettings * remoteIDSettings() const