8import QGroundControl.Controls
9import QGroundControl.FlightMap
10import QGroundControl.LogViewer
14 pageComponent: pageComponent
15 pageDescription: qsTr("Open and inspect DataFlash (.bin), PX4 ULog (.ulg), and telemetry (.tlog) logs in a unified workflow.")
23 height: availableHeight
24 spacing: ScreenTools.defaultFontPixelHeight
26 property string pendingBinFile: ""
28 readonly property bool _xAxisShowLocalTime: QGroundControl.settingsManager.logViewerSettings.xAxisShowLocalTime.rawValue
30 readonly property bool isFirmwareLog: logViewerController.sourceType === LogViewerController.Bin
31 || logViewerController.sourceType === LogViewerController.ULog
33 // Cancel any in-flight async parse before QML starts tearing down the tree.
34 // Without this, the background thread can emit signals (parseProgressChanged,
35 // parseFileFinished) into partially-destroyed QML objects, causing a
36 // QQmlData::disconnectNotifiers crash.
37 Component.onDestruction: logParser.clear()
39 function clearLoadedLogState(clearControllerState) {
40 replayController.isPlaying = false
41 replayController.link = null
43 logViewerController.setPlottableFields([])
44 logViewerController.clearSelection()
45 _parametersTab.applyFilter()
46 logViewerChart.clearMarker()
47 logViewerChart.refreshBinChart()
48 if (clearControllerState) {
49 logViewerController.clear()
53 function loadBinFile(file) {
54 if (logViewerController.hasLoadedLog) {
55 // Match explicit "Clear" behavior before loading replacement .bin file.
56 clearLoadedLogState(true)
59 logParser.startParsingAsync(file)
63 id: logViewerController
72 ignoreUnknownSignals: true
74 function onParseFileFinished(filePath, ok, errorMessage) {
75 if (filePath !== pendingBinFile) {
80 QGroundControl.showMessageDialog(logViewerPage, qsTr("Log Viewer"), errorMessage)
85 fieldsPanel.rebuildGroupedFields()
86 _parametersTab.applyFilter()
87 logViewerController.clearSelection()
88 logViewerChart.clearMarker()
89 logViewerChart.refreshBinChart()
90 Qt.callLater(logViewerChart.centerCursor)
92 const lowerPath = filePath.toLowerCase()
93 if (lowerPath.endsWith(".ulg")) {
94 logViewerController.openULogFile(filePath)
96 logViewerController.openBinLog(filePath)
102 LogReplayLinkController {
107 Layout.fillWidth: true
108 spacing: ScreenTools.defaultFontPixelWidth
111 text: qsTr("Open .bin")
112 visible: QGroundControl.hasAPMSupport
114 openDialog.nameFilters = ["DataFlash Logs (*.bin *.BIN *.log *.LOG)"]
115 openDialog.openForLoad()
120 text: qsTr("Open .ulg")
122 openDialog.nameFilters = ["PX4 ULog Files (*.ulg *.ULG)"]
123 openDialog.openForLoad()
128 text: qsTr("Open .tlog")
130 const activeVehicle = QGroundControl.multiVehicleManager.activeVehicle
131 if (activeVehicle && !activeVehicle.isOfflineEditingVehicle) {
132 QGroundControl.showMessageDialog(logViewerPage, qsTr("Log Viewer"), qsTr("Close active vehicle connections before starting telemetry replay."))
135 openDialog.nameFilters = ["Telemetry Logs (*.tlog *.TLOG)"]
136 openDialog.openForLoad()
142 enabled: logViewerController.hasLoadedLog
144 clearLoadedLogState(true)
149 Layout.fillWidth: true
150 elide: Text.ElideMiddle
151 text: logViewerController.hasLoadedLog ? logViewerController.currentLogPath.replace(/.*[/\\]/, "") : qsTr("No log selected")
155 visible: logViewerController.hasLoadedLog
156 text: qsTr("Start time:")
160 readonly property bool _hasStartTime: logParser.startTime
161 && !isNaN(logParser.startTime.getTime())
162 && logParser.startTime.getTime() > 0
163 visible: logViewerController.hasLoadedLog
165 ? Qt.formatDateTime(logParser.startTime, Qt.locale().dateTimeFormat(Locale.ShortFormat))
170 visible: logViewerController.hasLoadedLog && logParser.detectedVehicleType.length > 0
171 text: qsTr("Vehicle:")
175 visible: logViewerController.hasLoadedLog && logParser.detectedVehicleType.length > 0
176 text: logParser.detectedVehicleType
181 Layout.fillWidth: true
182 visible: logParser.parsing
183 spacing: ScreenTools.defaultFontPixelWidth
186 text: qsTr("Loading...")
190 Layout.fillWidth: true
193 value: logParser.parseProgress
199 Layout.fillWidth: true
200 visible: logViewerController.sourceType === LogViewerController.TLog && replayController.link
201 spacing: ScreenTools.defaultFontPixelWidth
204 text: replayController.isPlaying ? qsTr("Pause") : qsTr("Play")
205 onClicked: replayController.isPlaying = !replayController.isPlaying
213 ListElement { text: "0.1x"; value: 0.1 }
214 ListElement { text: "0.25x"; value: 0.25 }
215 ListElement { text: "0.5x"; value: 0.5 }
216 ListElement { text: "1x"; value: 1.0 }
217 ListElement { text: "2x"; value: 2.0 }
218 ListElement { text: "5x"; value: 5.0 }
219 ListElement { text: "10x"; value: 10.0 }
222 onActivated: (index) => replayController.playbackSpeed = model.get(index).value
225 QGCLabel { text: replayController.playheadTime }
229 Layout.fillWidth: true
233 property bool _internalUpdate: false
236 target: replayController
237 function onPercentCompleteChanged(percentComplete) {
238 _replaySlider._internalUpdate = true
239 _replaySlider.value = percentComplete
240 _replaySlider._internalUpdate = false
245 if (!_internalUpdate) {
246 replayController.percentComplete = value
251 QGCLabel { text: replayController.totalTime }
256 Layout.fillWidth: true
258 QGCTabButton { text: qsTr("Charting") }
259 QGCTabButton { text: qsTr("Map") }
260 QGCTabButton { text: qsTr("Parameters") }
261 QGCTabButton { text: qsTr("Messages") }
265 Layout.fillWidth: true
266 Layout.fillHeight: true
267 currentIndex: mainTabBar.currentIndex
269 // ---- Tab 0: Charting ----
271 spacing: ScreenTools.defaultFontPixelWidth
273 // Left panel: stats + fields list
274 LogViewerFieldsPanel {
276 Layout.fillHeight: true
278 logViewerController: logViewerController
279 onClearSelectedRequested: logViewerChart.refreshBinChart()
282 // Right panel: chart + MAVLink inspector
284 Layout.fillWidth: true
285 Layout.fillHeight: true
286 color: qgcPal.windowShadeDark
287 radius: ScreenTools.defaultFontPixelWidth * 0.5
291 anchors.margins: ScreenTools.defaultFontPixelWidth
292 spacing: ScreenTools.defaultFontPixelHeight * 0.5
296 Layout.fillWidth: true
297 Layout.fillHeight: true
298 visible: isFirmwareLog
300 logViewerController: logViewerController
301 xAxisShowLocalTime: _xAxisShowLocalTime
303 onCursorMoved: (t) => {
304 _mapTab._markerVisible = true
305 _mapTab._markerCoord = logParser.gpsCoordAt(t)
306 if (_altChart.visible) _altChart.setSharedCursor(t)
308 onZoomApplied: (minX, maxX) => {
309 if (_altChart.visible) _altChart.setSharedZoom(minX, maxX)
314 Layout.fillWidth: true
315 Layout.fillHeight: true
316 active: logViewerController.sourceType === LogViewerController.TLog
317 source: "qrc:/qml/QGroundControl/AnalyzeView/MAVLinkInspector/MAVLinkInspectorPage.qml"
323 // ---- Tab 1: Map ----
326 Layout.fillWidth: true
327 Layout.fillHeight: true
330 readonly property var _gpsPath: logParser.parseComplete ? logParser.gpsPath() : []
331 readonly property int _pathLen: (_gpsPath && _gpsPath.length) ? _gpsPath.length : 0
332 readonly property bool _hasPath: _pathLen >= 2
333 readonly property string _altFieldName: _hasPath ? logParser.gpsAltitudeFieldName() : ""
334 readonly property bool _hasAltField: _altFieldName.length > 0
336 // Shared cursor state (driven by altitude chart, displayed on map)
337 property bool _markerVisible: false
338 property var _markerCoord: ({})
346 Layout.fillWidth: true
347 Layout.fillHeight: true
352 mapName: "LogViewerMap"
353 allowGCSLocationCenter: true
355 readonly property var _path: _mapTab._gpsPath
357 function _fitPath() {
358 const p = _mapTab._gpsPath
359 if (!_mapTab._hasPath) return
360 var minLat = p[0].latitude, maxLat = minLat
361 var minLon = p[0].longitude, maxLon = minLon
362 for (var i = 1; i < p.length; i++) {
364 if (c.latitude < minLat) minLat = c.latitude
365 if (c.latitude > maxLat) maxLat = c.latitude
366 if (c.longitude < minLon) minLon = c.longitude
367 if (c.longitude > maxLon) maxLon = c.longitude
369 setVisibleRegion(QtPositioning.rectangle(
370 QtPositioning.coordinate(maxLat, minLon),
371 QtPositioning.coordinate(minLat, maxLon)))
376 function onParseCompleteChanged() {
377 if (logParser.parseComplete) Qt.callLater(_flightMap._fitPath)
383 line.color: QGroundControl.globalPalette.colorRed
384 path: _flightMap._path
387 // Position dot driven by altitude chart cursor
389 readonly property var _coord: _mapTab._markerCoord
390 visible: _mapTab._markerVisible && _mapTab._hasPath
391 && _coord && _coord.latitude !== undefined
392 coordinate: (_coord && _coord.latitude !== undefined)
393 ? QtPositioning.coordinate(_coord.latitude, _coord.longitude)
394 : QtPositioning.coordinate(0, 0)
395 anchorPoint: Qt.point(_posDot.width / 2, _posDot.height / 2)
396 sourceItem: Rectangle {
398 width: ScreenTools.defaultFontPixelHeight * 1.2
401 color: QGroundControl.globalPalette.colorYellow
402 border.color: "white"
408 anchors.margins: ScreenTools.defaultFontPixelWidth
409 anchors.left: parent.left
410 anchors.bottom: parent.bottom
411 mapControl: _flightMap
416 anchors.centerIn: parent
417 visible: logParser.parseComplete && !_mapTab._hasPath
418 text: qsTr("No GPS data found in this log")
423 anchors.centerIn: parent
424 visible: !logParser.parseComplete
425 text: qsTr("Load a log file to view the flight path")
430 // ---- Altitude chart ----
433 Layout.fillWidth: true
434 Layout.preferredHeight: ScreenTools.defaultFontPixelHeight * 14
435 visible: _mapTab._hasAltField && _mapTab._hasPath
437 altFieldName: _mapTab._altFieldName
438 xAxisShowLocalTime: _xAxisShowLocalTime
440 onMarkerChanged: (t) => {
441 _mapTab._markerVisible = true
442 _mapTab._markerCoord = logParser.gpsCoordAt(t)
443 logViewerChart.setSharedCursor(t)
446 _mapTab._markerVisible = false
448 onZoomApplied: (minX, maxX) => {
449 logViewerChart.setSharedZoom(minX, maxX)
455 // ---- Tab 2: Parameters ----
456 LogViewerParametersTab {
458 Layout.fillWidth: true
459 Layout.fillHeight: true
463 // ---- Tab 3: Messages ----
464 LogViewerMessagesTab {
466 Layout.fillWidth: true
467 Layout.fillHeight: true
474 title: qsTr("Select log file")
475 folder: QGroundControl.settingsManager.appSettings.logSavePath
478 onAcceptedForLoad: (file) => {
479 const fileLower = file.toLowerCase()
480 if (fileLower.endsWith(".tlog")) {
481 if (logViewerController.hasLoadedLog) {
482 clearLoadedLogState(true)
484 const replayLink = QGroundControl.linkManager.startLogReplay(file)
486 QGroundControl.showMessageDialog(
489 qsTr("Failed to start telemetry replay for the selected .tlog file.")
494 replayController.link = replayLink
495 logViewerController.openTLog(file)