QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
ULogFullHandler.cc
Go to the documentation of this file.
1#include "ULogFullHandler.h"
2
5
6#include <QtCore/QStringList>
7#include <QtCore/QTimeZone>
8
9#include <algorithm>
10#include <stdexcept>
11
12#include <ulog_cpp/subscription.hpp>
13
14QGC_LOGGING_CATEGORY(ULogFullHandlerLog, "AnalyzeView.ULogFullHandler")
15
16// Used for testing
17//#define QGC_NO_LOG_START_TIME
18
19namespace {
20
21QString _px4NavStateName(int state)
22{
23 switch (state) {
24 // Values from PX4 VehicleStatus.msg NAVIGATION_STATE_* constants
25 case 0: return QStringLiteral("Manual");
26 case 1: return QStringLiteral("Altitude");
27 case 2: return QStringLiteral("Position");
28 case 3: return QStringLiteral("Mission");
29 case 4: return QStringLiteral("Hold");
30 case 5: return QStringLiteral("Return");
31 case 6: return QStringLiteral("Position Slow");
32 case 8: return QStringLiteral("Altitude Cruise");
33 case 10: return QStringLiteral("Acro");
34 case 12: return QStringLiteral("Descend");
35 case 13: return QStringLiteral("Termination");
36 case 14: return QStringLiteral("Offboard");
37 case 15: return QStringLiteral("Stabilized");
38 case 17: return QStringLiteral("Takeoff");
39 case 18: return QStringLiteral("Land");
40 case 19: return QStringLiteral("Follow Target");
41 case 20: return QStringLiteral("Precision Land");
42 case 21: return QStringLiteral("Orbit");
43 case 22: return QStringLiteral("VTOL Takeoff");
44 default: return QStringLiteral("Mode %1").arg(state);
45 }
46}
47
48bool _isNumericScalarField(const ulog_cpp::Field &field)
49{
50 if (field.arrayLength() >= 0) {
51 return false; // arrays excluded from plottable fields
52 }
53 using BT = ulog_cpp::Field::BasicType;
54 switch (field.type().type) {
55 case BT::INT8:
56 case BT::UINT8:
57 case BT::INT16:
58 case BT::UINT16:
59 case BT::INT32:
60 case BT::UINT32:
61 case BT::INT64:
62 case BT::UINT64:
63 case BT::FLOAT:
64 case BT::DOUBLE:
65 case BT::BOOL:
66 return true;
67 default:
68 return false;
69 }
70}
71
72} // namespace
73
75 : _result(result)
76{
77}
78
79void ULogFullHandler::error(const std::string &msg, bool is_recoverable)
80{
81 const QString errorMessage = QString::fromStdString(msg);
82 if (!is_recoverable) {
83 _hadFatalError = true;
84 if (_result.errorMessage.isEmpty()) {
85 _result.errorMessage = errorMessage;
86 }
87 }
88 qCWarning(ULogFullHandlerLog) << "ULog parse error:" << errorMessage;
89}
90
91void ULogFullHandler::messageFormat(const ulog_cpp::MessageFormat &message_format)
92{
93 _formats[message_format.name()] = std::make_shared<ulog_cpp::MessageFormat>(message_format);
94}
95
96void ULogFullHandler::addLoggedMessage(const ulog_cpp::AddLoggedMessage &add_logged_message)
97{
98 const auto it = _formats.find(add_logged_message.messageName());
99 if (it != _formats.cend()) {
100 _subscriptions[add_logged_message.msgId()] = {
101 it->second,
102 add_logged_message.multiId(),
103 add_logged_message.messageName()
104 };
105 }
106}
107
109{
110 _headerComplete = true;
111 for (auto &[name, fmt] : _formats) {
112 fmt->resolveDefinition(_formats);
113 }
114}
115
116void ULogFullHandler::data(const ulog_cpp::Data &data)
117{
118 if (!_headerComplete) {
119 return;
120 }
121
122 const auto it = _subscriptions.find(data.msgId());
123 if (it == _subscriptions.cend()) {
124 return;
125 }
126
127 const SubscriptionInfo &sub = it->second;
128 if (!sub.format) {
129 return;
130 }
131
132 try {
133 const ulog_cpp::TypedDataView view(data, *sub.format);
134
135 // Extract timestamp (ULog convention: field named "timestamp", unit µs)
136 double timestampSecs = -1.0;
137 if (sub.format->fieldMap().count("timestamp") > 0) {
138 const uint64_t tsUs = view.at("timestamp").as<uint64_t>();
139 timestampSecs = static_cast<double>(tsUs) / 1e6;
140 _lastTimestampSecs = timestampSecs;
141 }
142
143 // Extract GPS UTC start time from first valid sensor_gps/vehicle_gps_position sample.
144 // Define QGC_NO_LOG_START_TIME at build time to suppress this for UI testing.
145#ifndef QGC_NO_LOG_START_TIME
146 if (_result.startTime.isNull() && timestampSecs >= 0.0) {
147 if ((sub.topicName == "sensor_gps" || sub.topicName == "vehicle_gps_position")
148 && sub.format->fieldMap().count("time_utc_usec") > 0) {
149 const uint64_t utcUsec = view.at("time_utc_usec").as<uint64_t>();
150 const uint64_t tsUs = view.at("timestamp").as<uint64_t>();
151 if (utcUsec > 0 && utcUsec >= tsUs) {
152 const qint64 startMs = static_cast<qint64>((utcUsec - tsUs) / 1000);
153 _result.startTime = QDateTime::fromMSecsSinceEpoch(startMs, QTimeZone::utc());
154 }
155 }
156 }
157#endif // QGC_NO_LOG_START_TIME
158
159 // Field name: "topic_name.field" or "topic_name[N].field" for multi-instance
160 const QString prefix = (sub.multiId > 0)
161 ? QStringLiteral("%1[%2].").arg(QString::fromStdString(sub.topicName)).arg(sub.multiId)
162 : QString::fromStdString(sub.topicName) + QLatin1Char('.');
163
164 for (const auto &field : sub.format->fields()) {
165 // Skip padding fields and the timestamp itself
166 if (field->name().rfind("_padding", 0) == 0) {
167 continue;
168 }
169 if (field->name() == "timestamp") {
170 continue;
171 }
172 if (!field->definitionResolved()) {
173 continue;
174 }
175
176 const QString fieldName = prefix + QString::fromStdString(field->name());
177 _fieldSet.insert(fieldName);
178
179 if (!_isNumericScalarField(*field) || timestampSecs < 0.0) {
180 continue;
181 }
182
183 const double value = view.at(field).as<double>();
184 _result.fieldSamples[fieldName].append(QPointF(timestampSecs, value));
185 _plottableFieldSet.insert(fieldName);
186 }
187
188 _result.sampleCount++;
189
190 if (timestampSecs >= 0.0) {
191 if (_result.minTimestamp < 0.0 || timestampSecs < _result.minTimestamp) {
192 _result.minTimestamp = timestampSecs;
193 }
194 _result.maxTimestamp = std::max(_result.maxTimestamp, timestampSecs);
195 }
196 } catch (const std::exception &e) {
197 qCWarning(ULogFullHandlerLog) << "Failed to decode data message:" << e.what();
198 }
199}
200
201void ULogFullHandler::logging(const ulog_cpp::Logging &logging)
202{
203 const double timestampSecs = static_cast<double>(logging.timestamp()) / 1e6;
204 const QString text = QString::fromStdString(logging.message());
205
206 if (text.isEmpty()) {
207 return;
208 }
209
210 QVariantMap msgRow;
211 msgRow[QStringLiteral("time")] = timestampSecs;
212 msgRow[QStringLiteral("text")] = text;
213 _result.messages.append(msgRow);
214
215 using Level = ulog_cpp::Logging::Level;
216 if (logging.logLevel() <= Level::Warning) {
217 const QString eventType = (logging.logLevel() <= Level::Error)
218 ? QStringLiteral("error")
219 : QStringLiteral("warning");
220
221 if (timestampSecs >= 0.0) {
222 QVariantMap eventRow;
223 eventRow[QStringLiteral("time")] = timestampSecs;
224 eventRow[QStringLiteral("type")] = eventType;
225 eventRow[QStringLiteral("description")] = text;
226 _result.events.append(eventRow);
227 }
228 }
229}
230
231void ULogFullHandler::parameter(const ulog_cpp::Parameter &param)
232{
233 const QString name = QString::fromStdString(param.field().name());
234 if (name.isEmpty()) {
235 return;
236 }
237
238 QVariant value;
239 bool isFloat = false;
240 try {
241 // ULog parameters are restricted to int32_t and float per spec.
242 // as<double>() safely static_casts either type.
243 value = param.value().as<double>();
244 isFloat = (param.field().type().type == ulog_cpp::Field::BasicType::FLOAT);
245 } catch (const std::exception &) {
246 value = QVariant();
247 }
248
249 QVariantMap row;
250 row[QStringLiteral("name")] = name;
251 row[QStringLiteral("value")] = value;
252 row[QStringLiteral("isFloat")] = isFloat;
253 // hasDefault / defaultValue / isDefault are filled in by finalize() once all
254 // ParameterDefault messages have been collected.
255 _result.parameters.append(row);
256}
257
258void ULogFullHandler::parameterDefault(const ulog_cpp::ParameterDefault &param_default)
259{
260 const QString name = QString::fromStdString(param_default.field().name());
261 if (name.isEmpty()) {
262 return;
263 }
264 try {
265 const double defaultVal = param_default.value().as<double>();
266 // Only store system defaults (bit 0 set). Configuration defaults (bit 1) are
267 // user-visible overrides and not what we want to compare against.
268 using DT = ulog_cpp::ulog_parameter_default_type_t;
269 if ((static_cast<uint8_t>(param_default.defaultType()) &
270 static_cast<uint8_t>(DT::system)) != 0) {
271 _paramDefaults[name] = defaultVal;
272 }
273 } catch (const std::exception &) {
274 // Ignore unreadable defaults
275 }
276}
277
278void ULogFullHandler::dropout(const ulog_cpp::Dropout &dropout)
279{
280 if (_lastTimestampSecs < 0.0) {
281 return;
282 }
283
284 const double start = _lastTimestampSecs;
285 const double end = start + static_cast<double>(dropout.durationMs()) / 1000.0;
286 QVariantMap row;
287 row[QStringLiteral("start")] = start;
288 row[QStringLiteral("end")] = end;
289 _result.dropouts.append(row);
290}
291
293{
294 // Detect vehicle type from vehicle_status.vehicle_type
295 // PX4 vehicle_type enum: 0=Unknown, 1=Rotary Wing, 2=Fixed Wing, 3=Rover, 4=Airship
296 const auto vehicleTypeIt = _result.fieldSamples.constFind(QStringLiteral("vehicle_status.vehicle_type"));
297 if (vehicleTypeIt != _result.fieldSamples.cend() && !vehicleTypeIt->isEmpty()) {
298 const int vtype = static_cast<int>(vehicleTypeIt->first().y());
299 switch (vtype) {
300 case 1: _result.detectedVehicleType = QStringLiteral("Multirotor/Helicopter"); break;
301 case 2: _result.detectedVehicleType = QStringLiteral("Fixed Wing"); break;
302 case 3: _result.detectedVehicleType = QStringLiteral("Rover"); break;
303 case 4: _result.detectedVehicleType = QStringLiteral("Airship"); break;
304 default: break; // 0 = Unknown, leave empty so UI shows "Unknown"
305 }
306 }
307
308 // Derive mode segments from vehicle_status.nav_state samples.
309 // nav_state is a uint8_t mapped to the PX4 navigation_state enum.
310 const auto navStateIt = _result.fieldSamples.constFind(QStringLiteral("vehicle_status.nav_state"));
311 if (navStateIt != _result.fieldSamples.cend()) {
312 const QVector<QPointF> &samples = navStateIt.value();
313 int lastNavState = -1;
314 double segmentStart = -1.0;
315 QString segmentMode;
316
317 for (const QPointF &pt : samples) {
318 const int navState = static_cast<int>(pt.y());
319 if (navState != lastNavState) {
320 // Close the previous segment
321 if (lastNavState >= 0 && segmentStart >= 0.0) {
322 QVariantMap seg;
323 seg[QStringLiteral("mode")] = segmentMode;
324 seg[QStringLiteral("start")] = segmentStart;
325 seg[QStringLiteral("end")] = pt.x();
326 _result.modeSegments.append(seg);
327 }
328 lastNavState = navState;
329 segmentStart = pt.x();
330 segmentMode = _px4NavStateName(navState);
331 }
332 }
333
334 // Close the final open segment
335 if (lastNavState >= 0 && segmentStart >= 0.0 && _result.maxTimestamp >= segmentStart) {
336 QVariantMap seg;
337 seg[QStringLiteral("mode")] = segmentMode;
338 seg[QStringLiteral("start")] = segmentStart;
339 seg[QStringLiteral("end")] = _result.maxTimestamp;
340 _result.modeSegments.append(seg);
341 }
342 }
343
344 // Sort field lists for consistent display
345 _result.availableFields = _fieldSet.values();
346 std::sort(_result.availableFields.begin(), _result.availableFields.end());
347 _result.plottableFields = _plottableFieldSet.values();
348 std::sort(_result.plottableFields.begin(), _result.plottableFields.end());
349
350 // Annotate parameter rows with default value info now that all ParameterDefault
351 // messages have been collected.
352 for (int i = 0; i < _result.parameters.size(); i++) {
353 QVariantMap row = _result.parameters[i].toMap();
354 const QString name = row.value(QStringLiteral("name")).toString();
355 const auto it = _paramDefaults.constFind(name);
356 if (it != _paramDefaults.constEnd()) {
357 const double defaultVal = it.value();
358 const QVariant valueVariant = row.value(QStringLiteral("value"));
359 row[QStringLiteral("hasDefault")] = true;
360 row[QStringLiteral("defaultValue")] = defaultVal;
361 // Treat parameters whose value couldn't be read (invalid QVariant from
362 // the catch block in parameter()) as non-default so they are never
363 // hidden by the "Changed only" filter.
364 bool isDefault = false;
365 if (valueVariant.isValid() && valueVariant.canConvert<double>()) {
366 const double currentVal = valueVariant.toDouble();
367 // qFuzzyCompare is undefined when either value is 0.0, so use exact
368 // equality for the zero case and relative comparison otherwise.
369 isDefault = (currentVal == defaultVal)
370 || (!qFuzzyIsNull(defaultVal) && qFuzzyCompare(currentVal, defaultVal));
371 }
372 row[QStringLiteral("isDefault")] = isDefault;
373 } else {
374 row[QStringLiteral("hasDefault")] = false;
375 row[QStringLiteral("defaultValue")] = QVariant();
376 row[QStringLiteral("isDefault")] = false;
377 }
378 _result.parameters[i] = row;
379 }
380
382 _result.ok = true;
383}
std::function< void(float)> ProgressCallback
#define QGC_LOGGING_CATEGORY(name, categoryStr)
void parameter(const ulog_cpp::Parameter &parameter) override
ULogFullHandler(LogParseResult &result, const ProgressCallback &progressCallback=nullptr)
void addLoggedMessage(const ulog_cpp::AddLoggedMessage &add_logged_message) override
void error(const std::string &msg, bool is_recoverable) override
void logging(const ulog_cpp::Logging &logging) override
void messageFormat(const ulog_cpp::MessageFormat &message_format) override
void parameterDefault(const ulog_cpp::ParameterDefault &parameter_default) override
void data(const ulog_cpp::Data &data) override
void dropout(const ulog_cpp::Dropout &dropout) override
void headerComplete() override
QVariantList modeSegments
QStringList plottableFields
QStringList availableFields
QHash< QString, QVector< QPointF > > fieldSamples