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 _isNumericBasicType(ulog_cpp::Field::BasicType type)
49{
50 using BT = ulog_cpp::Field::BasicType;
51 switch (type) {
52 case BT::INT8:
53 case BT::UINT8:
54 case BT::INT16:
55 case BT::UINT16:
56 case BT::INT32:
57 case BT::UINT32:
58 case BT::INT64:
59 case BT::UINT64:
60 case BT::FLOAT:
61 case BT::DOUBLE:
62 case BT::BOOL:
63 return true;
64 default:
65 return false;
66 }
67}
68
69} // namespace
70
72 : _result(result)
73{
74}
75
76void ULogFullHandler::error(const std::string &msg, bool is_recoverable)
77{
78 const QString errorMessage = QString::fromStdString(msg);
79 if (!is_recoverable) {
80 _hadFatalError = true;
81 if (_result.errorMessage.isEmpty()) {
82 _result.errorMessage = errorMessage;
83 }
84 }
85 qCWarning(ULogFullHandlerLog) << "ULog parse error:" << errorMessage;
86}
87
88void ULogFullHandler::fileHeader(const ulog_cpp::FileHeader& header)
89{
90 _logStartSecs = static_cast<double>(header.header().timestamp) / 1e6;
91}
92
93void ULogFullHandler::messageFormat(const ulog_cpp::MessageFormat &message_format)
94{
95 _formats[message_format.name()] = std::make_shared<ulog_cpp::MessageFormat>(message_format);
96}
97
98void ULogFullHandler::addLoggedMessage(const ulog_cpp::AddLoggedMessage &add_logged_message)
99{
100 const auto it = _formats.find(add_logged_message.messageName());
101 if (it != _formats.cend()) {
102 _subscriptions[add_logged_message.msgId()] = {
103 it->second,
104 add_logged_message.multiId(),
105 add_logged_message.messageName()
106 };
107 }
108}
109
111{
112 _headerComplete = true;
113 for (auto &[name, fmt] : _formats) {
114 fmt->resolveDefinition(_formats);
115 }
116}
117
118void ULogFullHandler::data(const ulog_cpp::Data &data)
119{
120 if (!_headerComplete) {
121 return;
122 }
123
124 const auto it = _subscriptions.find(data.msgId());
125 if (it == _subscriptions.cend()) {
126 return;
127 }
128
129 const SubscriptionInfo &sub = it->second;
130 if (!sub.format) {
131 return;
132 }
133
134 try {
135 const ulog_cpp::TypedDataView view(data, *sub.format);
136
137 // Extract timestamp (ULog convention: field named "timestamp", unit µs)
138 double timestampSecs = -1.0;
139 if (sub.format->fieldMap().count("timestamp") > 0) {
140 const uint64_t tsUs = view.at("timestamp").as<uint64_t>();
141 timestampSecs = static_cast<double>(tsUs) / 1e6;
142 // The logger writes each topic's last value at log start with its original, older timestamp
143 if (timestampSecs < _logStartSecs) {
144 timestampSecs = _logStartSecs;
145 _staleSampleCount++;
146 }
147 _lastTimestampSecs = timestampSecs;
148 }
149
150 // Extract GPS UTC start time from first valid sensor_gps/vehicle_gps_position sample.
151 // Define QGC_NO_LOG_START_TIME at build time to suppress this for UI testing.
152#ifndef QGC_NO_LOG_START_TIME
153 if (_result.startTime.isNull() && timestampSecs >= 0.0) {
154 if ((sub.topicName == "sensor_gps" || sub.topicName == "vehicle_gps_position")
155 && sub.format->fieldMap().count("time_utc_usec") > 0) {
156 const uint64_t utcUsec = view.at("time_utc_usec").as<uint64_t>();
157 const uint64_t tsUs = view.at("timestamp").as<uint64_t>();
158 if (utcUsec > 0 && utcUsec >= tsUs) {
159 const qint64 startMs = static_cast<qint64>((utcUsec - tsUs) / 1000);
160 _result.startTime = QDateTime::fromMSecsSinceEpoch(startMs, QTimeZone::utc());
161 }
162 }
163 }
164#endif // QGC_NO_LOG_START_TIME
165
166 // Field name: "topic_name.field" or "topic_name[N].field" for multi-instance
167 const QString prefix = (sub.multiId > 0)
168 ? QStringLiteral("%1[%2].").arg(QString::fromStdString(sub.topicName)).arg(sub.multiId)
169 : QString::fromStdString(sub.topicName) + QLatin1Char('.');
170
171 for (const auto &field : sub.format->fields()) {
172 // Skip padding fields and the timestamp itself
173 if (field->name().rfind("_padding", 0) == 0) {
174 continue;
175 }
176 if (field->name() == "timestamp") {
177 continue;
178 }
179 if (!field->definitionResolved()) {
180 continue;
181 }
182
183 const QString fieldName = prefix + QString::fromStdString(field->name());
184 _collectFieldSamples(fieldName, *field, view.at(field), timestampSecs);
185 }
186
187 _result.sampleCount++;
188
189 if (timestampSecs >= 0.0) {
190 if (_result.minTimestamp < 0.0 || timestampSecs < _result.minTimestamp) {
191 _result.minTimestamp = timestampSecs;
192 }
193 _result.maxTimestamp = std::max(_result.maxTimestamp, timestampSecs);
194 }
195 } catch (const std::exception &e) {
196 qCWarning(ULogFullHandlerLog) << "Failed to decode data message:" << e.what();
197 }
198}
199
200void ULogFullHandler::_collectFieldSamples(const QString& fieldName, const ulog_cpp::Field& field,
201 const ulog_cpp::Value& value, double timestampSecs)
202{
203 if (field.arrayLength() < 0) {
204 _collectElementSamples(fieldName, field, value, timestampSecs);
205 return;
206 }
207
208 // char arrays are strings, not per-element signals
209 if (field.type().type == ulog_cpp::Field::BasicType::CHAR) {
210 _fieldSet.insert(fieldName);
211 return;
212 }
213
214 for (int i = 0; i < field.arrayLength(); i++) {
215 _collectElementSamples(QStringLiteral("%1[%2]").arg(fieldName).arg(i), field, value[static_cast<size_t>(i)],
216 timestampSecs);
217 }
218}
219
220void ULogFullHandler::_collectElementSamples(const QString& fieldName, const ulog_cpp::Field& field,
221 const ulog_cpp::Value& value, double timestampSecs)
222{
223 if (field.type().type == ulog_cpp::Field::BasicType::NESTED) {
224 for (const auto& nestedField : field.nestedFormat()->fields()) {
225 if (nestedField->name().rfind("_padding", 0) == 0) {
226 continue;
227 }
228 if (!nestedField->definitionResolved()) {
229 continue;
230 }
231 const QString nestedFieldName = fieldName + QLatin1Char('.') + QString::fromStdString(nestedField->name());
232 _collectFieldSamples(nestedFieldName, *nestedField, value[*nestedField], timestampSecs);
233 }
234 return;
235 }
236
237 _fieldSet.insert(fieldName);
238
239 if (!_isNumericBasicType(field.type().type) || (timestampSecs < 0.0)) {
240 return;
241 }
242
243 _result.fieldSamples[fieldName].append(QPointF(timestampSecs, value.as<double>()));
244 _plottableFieldSet.insert(fieldName);
245}
246
247void ULogFullHandler::logging(const ulog_cpp::Logging &logging)
248{
249 const double timestampSecs = static_cast<double>(logging.timestamp()) / 1e6;
250 const QString text = QString::fromStdString(logging.message());
251
252 if (text.isEmpty()) {
253 return;
254 }
255
256 QVariantMap msgRow;
257 msgRow[QStringLiteral("time")] = timestampSecs;
258 msgRow[QStringLiteral("text")] = text;
259 _result.messages.append(msgRow);
260
261 using Level = ulog_cpp::Logging::Level;
262 if (logging.logLevel() <= Level::Warning) {
263 const QString eventType = (logging.logLevel() <= Level::Error)
264 ? QStringLiteral("error")
265 : QStringLiteral("warning");
266
267 if (timestampSecs >= 0.0) {
268 QVariantMap eventRow;
269 eventRow[QStringLiteral("time")] = timestampSecs;
270 eventRow[QStringLiteral("type")] = eventType;
271 eventRow[QStringLiteral("description")] = text;
272 _result.events.append(eventRow);
273 }
274 }
275}
276
277void ULogFullHandler::parameter(const ulog_cpp::Parameter &param)
278{
279 const QString name = QString::fromStdString(param.field().name());
280 if (name.isEmpty()) {
281 return;
282 }
283
284 QVariant value;
285 bool isFloat = false;
286 try {
287 // ULog parameters are restricted to int32_t and float per spec.
288 // as<double>() safely static_casts either type.
289 value = param.value().as<double>();
290 isFloat = (param.field().type().type == ulog_cpp::Field::BasicType::FLOAT);
291 } catch (const std::exception &) {
292 value = QVariant();
293 }
294
295 QVariantMap row;
296 row[QStringLiteral("name")] = name;
297 row[QStringLiteral("value")] = value;
298 row[QStringLiteral("isFloat")] = isFloat;
299 // hasDefault / defaultValue / isDefault are filled in by finalize() once all
300 // ParameterDefault messages have been collected.
301 _result.parameters.append(row);
302}
303
304void ULogFullHandler::parameterDefault(const ulog_cpp::ParameterDefault &param_default)
305{
306 const QString name = QString::fromStdString(param_default.field().name());
307 if (name.isEmpty()) {
308 return;
309 }
310 try {
311 const double defaultVal = param_default.value().as<double>();
312 // Only store system defaults (bit 0 set). Configuration defaults (bit 1) are
313 // user-visible overrides and not what we want to compare against.
314 using DT = ulog_cpp::ulog_parameter_default_type_t;
315 if ((static_cast<uint8_t>(param_default.defaultType()) &
316 static_cast<uint8_t>(DT::system)) != 0) {
317 _paramDefaults[name] = defaultVal;
318 }
319 } catch (const std::exception &) {
320 // Ignore unreadable defaults
321 }
322}
323
324void ULogFullHandler::dropout(const ulog_cpp::Dropout &dropout)
325{
326 if (_lastTimestampSecs < 0.0) {
327 return;
328 }
329
330 const double start = _lastTimestampSecs;
331 const double end = start + static_cast<double>(dropout.durationMs()) / 1000.0;
332 QVariantMap row;
333 row[QStringLiteral("start")] = start;
334 row[QStringLiteral("end")] = end;
335 _result.dropouts.append(row);
336}
337
339{
340 if (_staleSampleCount > 0) {
341 qCDebug(ULogFullHandlerLog) << "Moved" << _staleSampleCount << "samples older than log start to"
342 << _logStartSecs << "s";
343 }
344
345 // Detect vehicle type from vehicle_status.vehicle_type
346 // PX4 vehicle_type enum: 0=Unknown, 1=Rotary Wing, 2=Fixed Wing, 3=Rover, 4=Airship
347 const auto vehicleTypeIt = _result.fieldSamples.constFind(QStringLiteral("vehicle_status.vehicle_type"));
348 if (vehicleTypeIt != _result.fieldSamples.cend() && !vehicleTypeIt->isEmpty()) {
349 const int vtype = static_cast<int>(vehicleTypeIt->first().y());
350 switch (vtype) {
351 case 1: _result.detectedVehicleType = QStringLiteral("Multirotor/Helicopter"); break;
352 case 2: _result.detectedVehicleType = QStringLiteral("Fixed Wing"); break;
353 case 3: _result.detectedVehicleType = QStringLiteral("Rover"); break;
354 case 4: _result.detectedVehicleType = QStringLiteral("Airship"); break;
355 default: break; // 0 = Unknown, leave empty so UI shows "Unknown"
356 }
357 }
358
359 // Derive mode segments from vehicle_status.nav_state samples.
360 // nav_state is a uint8_t mapped to the PX4 navigation_state enum.
361 const auto navStateIt = _result.fieldSamples.constFind(QStringLiteral("vehicle_status.nav_state"));
362 if (navStateIt != _result.fieldSamples.cend()) {
363 const QVector<QPointF> &samples = navStateIt.value();
364 int lastNavState = -1;
365 double segmentStart = -1.0;
366 QString segmentMode;
367
368 for (const QPointF &pt : samples) {
369 const int navState = static_cast<int>(pt.y());
370 if (navState != lastNavState) {
371 // Close the previous segment
372 if (lastNavState >= 0 && segmentStart >= 0.0) {
373 QVariantMap seg;
374 seg[QStringLiteral("mode")] = segmentMode;
375 seg[QStringLiteral("start")] = segmentStart;
376 seg[QStringLiteral("end")] = pt.x();
377 _result.modeSegments.append(seg);
378 }
379 lastNavState = navState;
380 segmentStart = pt.x();
381 segmentMode = _px4NavStateName(navState);
382 }
383 }
384
385 // Close the final open segment
386 if (lastNavState >= 0 && segmentStart >= 0.0 && _result.maxTimestamp >= segmentStart) {
387 QVariantMap seg;
388 seg[QStringLiteral("mode")] = segmentMode;
389 seg[QStringLiteral("start")] = segmentStart;
390 seg[QStringLiteral("end")] = _result.maxTimestamp;
391 _result.modeSegments.append(seg);
392 }
393 }
394
395 // Sort field lists for consistent display
396 _result.availableFields = _fieldSet.values();
397 std::sort(_result.availableFields.begin(), _result.availableFields.end());
398 _result.plottableFields = _plottableFieldSet.values();
399 std::sort(_result.plottableFields.begin(), _result.plottableFields.end());
400
401 // Annotate parameter rows with default value info now that all ParameterDefault
402 // messages have been collected.
403 for (int i = 0; i < _result.parameters.size(); i++) {
404 QVariantMap row = _result.parameters[i].toMap();
405 const QString name = row.value(QStringLiteral("name")).toString();
406 const auto it = _paramDefaults.constFind(name);
407 if (it != _paramDefaults.constEnd()) {
408 const double defaultVal = it.value();
409 const QVariant valueVariant = row.value(QStringLiteral("value"));
410 row[QStringLiteral("hasDefault")] = true;
411 row[QStringLiteral("defaultValue")] = defaultVal;
412 // Treat parameters whose value couldn't be read (invalid QVariant from
413 // the catch block in parameter()) as non-default so they are never
414 // hidden by the "Changed only" filter.
415 bool isDefault = false;
416 if (valueVariant.isValid() && valueVariant.canConvert<double>()) {
417 const double currentVal = valueVariant.toDouble();
418 // qFuzzyCompare is undefined when either value is 0.0, so use exact
419 // equality for the zero case and relative comparison otherwise.
420 isDefault = (currentVal == defaultVal)
421 || (!qFuzzyIsNull(defaultVal) && qFuzzyCompare(currentVal, defaultVal));
422 }
423 row[QStringLiteral("isDefault")] = isDefault;
424 } else {
425 row[QStringLiteral("hasDefault")] = false;
426 row[QStringLiteral("defaultValue")] = QVariant();
427 row[QStringLiteral("isDefault")] = false;
428 }
429 _result.parameters[i] = row;
430 }
431
433 _result.ok = true;
434}
std::function< void(float)> ProgressCallback
#define QGC_LOGGING_CATEGORY(name, categoryStr)
void parameter(const ulog_cpp::Parameter &parameter) override
void fileHeader(const ulog_cpp::FileHeader &header) 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