7import QGroundControl.Controls
11 pageComponent: pageComponent
12 pageDescription: qsTr("Open and inspect DataFlash (.bin), PX4 ULog (.ulg), and telemetry (.tlog) logs in a unified workflow.")
19 height: availableHeight
20 spacing: ScreenTools.defaultFontPixelHeight
22 property bool binLoading: false
23 property string pendingBinFile: ""
24 property bool cursorVisible: false
25 property real cursorPixelX: 0
26 property real cursorXValue: 0
27 property real cursorPopupY: 0
28 property var cursorRows: []
29 property var cursorEventRows: []
30 property string cursorModeName: ""
31 property string fieldSearchText: ""
32 property string parameterSearchText: ""
33 property var filteredFieldRows: []
34 property var filteredParameters: []
35 property real fullMinX: 0
36 property real fullMaxX: 1
37 property real zoomMinX: 0
38 property real zoomMaxX: 1
40 // Maps fieldName → LineSeries and fieldName → {min, max} for incremental add/remove.
41 property var _seriesByField: ({})
42 property var _fieldYRange: ({})
43 // Maps eventType → ScatterSeries; rebuilt whenever field selection changes,
44 // because Y pin position depends on binYAxis.max which is recomputed at that time.
45 property var _eventSeriesByType: ({})
47 // Cap decimation to keep QtGraphs rendering below ~16ms per frame.
48 // Empirically, >~6000 points per series causes visible frame drops on mid-range hardware.
49 property int maxChartPointsPerField: 6000
51 readonly property bool isFirmwareLog: logViewerController.sourceType === LogViewerController.Bin
52 || logViewerController.sourceType === LogViewerController.ULog
54 // Visually distinct categorical palette for selected-field series.
55 // Position-based picks avoid hash collisions producing similar colors.
56 readonly property var _fieldChartColors: [
66 "#546E7A", // blue grey
72 id: lineSeriesComponent
78 id: scatterSeriesComponent
83 function eventColor(eventType) {
84 return logViewerController.eventColor(eventType)
87 function eventTypeLabel(eventType) {
88 if (eventType === "mode") {
91 if (eventType === "event") {
94 if (eventType === "error") {
97 if (eventType === "warning") {
98 return qsTr("Warning")
103 function modeColor(modeName) {
104 return logViewerController.modeColor(String(modeName))
107 function modeLegendEntries() {
108 return logViewerController.modeLegendEntries(logParser.modeSegments)
111 function rebuildGroupedFields() {
112 logViewerController.setPlottableFields(logParser.plottableFields)
116 function applyFieldFilter() {
117 const query = String(fieldSearchText).trim().toLowerCase()
118 if (query.length === 0) {
119 filteredFieldRows = logViewerController.fieldRows
123 const groupedMap = {}
124 const fields = logParser.plottableFields
125 for (let i = 0; i < fields.length; i++) {
126 const fullName = String(fields[i])
127 const splitIndex = fullName.indexOf(".")
128 const groupName = splitIndex > 0 ? fullName.substring(0, splitIndex) : qsTr("Other")
129 const shortName = splitIndex > 0 ? fullName.substring(splitIndex + 1) : fullName
130 const haystack = (fullName + " " + groupName + " " + shortName).toLowerCase()
131 if (haystack.indexOf(query) === -1) {
135 if (!groupedMap[groupName]) {
136 groupedMap[groupName] = []
138 groupedMap[groupName].push({ fullName: fullName, shortName: shortName })
141 const groups = Object.keys(groupedMap).sort()
143 for (let g = 0; g < groups.length; g++) {
144 const groupName = groups[g]
145 rows.push({ rowType: "group", group: groupName })
146 groupedMap[groupName].sort((a, b) => String(a.shortName).localeCompare(String(b.shortName)))
147 for (let s = 0; s < groupedMap[groupName].length; s++) {
151 fullName: groupedMap[groupName][s].fullName,
152 shortName: groupedMap[groupName][s].shortName
156 filteredFieldRows = rows
159 function applyParameterFilter() {
160 const query = String(parameterSearchText).trim().toLowerCase()
161 if (query.length === 0) {
162 filteredParameters = logParser.parameters
167 for (let i = 0; i < logParser.parameters.length; i++) {
168 const item = logParser.parameters[i]
169 const name = String(item.name)
170 const value = String(item.value)
171 if ((name + " " + value).toLowerCase().indexOf(query) !== -1) {
175 filteredParameters = output
178 function isGroupExpanded(groupName) {
179 return logViewerController.isGroupExpanded(groupName)
182 function toggleGroupExpanded(groupName) {
183 if (String(fieldSearchText).trim().length > 0) {
186 logViewerController.toggleGroupExpanded(groupName)
190 function isFieldSelected(fieldName) {
191 return logViewerController.isFieldSelected(fieldName)
194 function fieldColor(fieldName) {
195 const idx = logViewerController.selectedFields.indexOf(fieldName)
197 return _fieldChartColors[idx % _fieldChartColors.length]
199 return logViewerController.fieldColor(fieldName)
202 function applyZoomRange(minX, maxX) {
208 binXAxis.min = zoomMinX
209 binXAxis.max = zoomMaxX
212 function resetZoom() {
213 applyZoomRange(fullMinX, fullMaxX)
216 function _pixelToAxisX(pixelX) {
217 const plotX = binChart.plotArea.x
218 const plotW = binChart.plotArea.width
219 if (plotW <= 0 || binXAxis.max <= binXAxis.min) {
223 const clampedPixel = Math.max(plotX, Math.min(plotX + plotW, pixelX))
224 const ratio = (clampedPixel - plotX) / plotW
225 return binXAxis.min + (ratio * (binXAxis.max - binXAxis.min))
228 function updateCursorInfo(pixelX, pixelY, width, height) {
229 const selectedFields = logViewerController.selectedFields
230 if (selectedFields.length === 0 || width <= 0 || height <= 0) {
231 cursorVisible = false
236 cursorPixelX = Math.max(binChart.plotArea.x, Math.min(binChart.plotArea.x + binChart.plotArea.width, pixelX))
237 cursorXValue = _pixelToAxisX(cursorPixelX)
238 cursorPopupY = Math.max(0, Math.min(height - (ScreenTools.defaultFontPixelHeight * 4), pixelY))
239 cursorModeName = logParser.modeAt(cursorXValue)
242 for (let i = 0; i < selectedFields.length; i++) {
243 const field = selectedFields[i]
244 const value = logParser.fieldValueAt(field, cursorXValue)
250 color: fieldColor(field),
256 const threshold = Math.max(0.05, (binXAxis.max - binXAxis.min) / 200.0)
257 const nearbyEvents = logParser.eventsNear(cursorXValue, threshold)
259 for (let i = 0; i < nearbyEvents.length; i++) {
261 color: eventColor(nearbyEvents[i].type),
262 text: nearbyEvents[i].description
265 cursorEventRows = events
268 function clearLoadedLogState(clearControllerState) {
269 replayController.isPlaying = false
270 replayController.link = null
272 logViewerController.setPlottableFields([])
273 logViewerController.clearSelection()
275 filteredFieldRows = []
276 filteredParameters = []
278 if (clearControllerState) {
279 logViewerController.clear()
283 function loadBinFile(file) {
284 if (logViewerController.hasLoadedLog) {
285 // Match explicit "Clear" behavior before loading replacement .bin file.
286 clearLoadedLogState(true)
288 pendingBinFile = file
290 parseStartTimer.start()
293 function _executePendingBinParse() {
294 if (!pendingBinFile || pendingBinFile.length === 0) {
299 const file = pendingBinFile
300 logParser.parseFileAsync(file)
303 // Full reset: remove all series and reinitialise axes from the log time range.
304 // Called on log load, on full clear (clearLoadedLogState), and when all selected fields
305 // are cleared via the "Clear Selected" button. For incremental selection changes use
306 // _syncSeriesWithSelection instead.
307 function refreshBinChart() {
308 while (binChart.seriesList.length > 0) {
309 binChart.removeSeries(binChart.seriesList[0])
313 _eventSeriesByType = {}
315 if (logParser.minTimestamp >= 0.0 && logParser.maxTimestamp > logParser.minTimestamp) {
316 fullMinX = logParser.minTimestamp
317 fullMaxX = logParser.maxTimestamp
324 binXAxis.min = zoomMinX
325 binXAxis.max = zoomMaxX
330 // Incremental update: diff the current series map against the new selection,
331 // remove series for deselected fields, add series for newly selected fields,
332 // then recompute the Y axis and rebuild the event scatter series.
333 function _syncSeriesWithSelection() {
334 const newSelection = logViewerController.selectedFields
336 // Build lookup of desired fields
338 for (let i = 0; i < newSelection.length; i++) {
339 desired[String(newSelection[i])] = true
342 // Remove series for fields that are no longer selected
343 const tracked = Object.keys(_seriesByField)
344 for (let i = 0; i < tracked.length; i++) {
345 if (!desired[tracked[i]]) {
346 binChart.removeSeries(_seriesByField[tracked[i]])
347 delete _seriesByField[tracked[i]]
348 delete _fieldYRange[tracked[i]]
352 // Add a series for each newly selected field
353 for (let i = 0; i < newSelection.length; i++) {
354 const fieldName = String(newSelection[i])
355 if (_seriesByField[fieldName]) {
359 const points = logParser.fieldSamples(fieldName)
360 if (!points || points.length === 0) {
364 const series = lineSeriesComponent.createObject(binChart, {
365 color: fieldColor(fieldName),
370 binChart.addSeries(series)
372 let minY = Number.MAX_VALUE
373 let maxY = -Number.MAX_VALUE
374 const sampleStep = Math.max(1, Math.ceil(points.length / maxChartPointsPerField))
375 let appendedLastX = -Number.MAX_VALUE
376 for (let j = 0; j < points.length; j += sampleStep) {
377 series.append(points[j].x, points[j].y)
378 appendedLastX = points[j].x
379 if (points[j].y < minY) minY = points[j].y
380 if (points[j].y > maxY) maxY = points[j].y
382 if (sampleStep > 1) {
383 const last = points[points.length - 1]
384 if (last && last.x !== appendedLastX) {
385 series.append(last.x, last.y)
389 _seriesByField[fieldName] = series
390 _fieldYRange[fieldName] = { min: minY, max: maxY }
393 // Update colors for all tracked series (selection indices may have shifted)
394 for (let i = 0; i < newSelection.length; i++) {
395 const fn = String(newSelection[i])
396 const s = _seriesByField[fn]
397 if (s) s.color = fieldColor(fn)
400 // Recompute Y axis from cached per-field ranges
401 const allTracked = Object.keys(_seriesByField)
404 if (allTracked.length > 0) {
405 globalMinY = Number.MAX_VALUE
406 globalMaxY = -Number.MAX_VALUE
407 for (let i = 0; i < allTracked.length; i++) {
408 const r = _fieldYRange[allTracked[i]]
410 if (r.min < globalMinY) globalMinY = r.min
411 if (r.max > globalMaxY) globalMaxY = r.max
414 if (globalMinY === globalMaxY) globalMaxY = globalMinY + 1
416 binYAxis.min = globalMinY
417 binYAxis.max = globalMaxY
419 // Rebuild event scatter series (Y position is pinned to current binYAxis.max)
420 const existingTypes = Object.keys(_eventSeriesByType)
421 for (let i = 0; i < existingTypes.length; i++) {
422 binChart.removeSeries(_eventSeriesByType[existingTypes[i]])
424 _eventSeriesByType = {}
425 const eventList = logParser.events
426 for (let e = 0; e < eventList.length; e++) {
427 const ev = eventList[e]
428 if (ev.time < binXAxis.min || ev.time > binXAxis.max) {
431 if (!_eventSeriesByType[ev.type]) {
432 const eventSeries = scatterSeriesComponent.createObject(binChart, {
433 color: eventColor(ev.type),
437 binChart.addSeries(eventSeries)
438 _eventSeriesByType[ev.type] = eventSeries
440 _eventSeriesByType[ev.type].append(ev.time, binYAxis.max)
444 LogViewerController {
445 id: logViewerController
453 target: logViewerController
454 function onFieldRowsChanged() {
457 function onSelectedFieldsChanged() {
458 Qt.callLater(_syncSeriesWithSelection)
464 ignoreUnknownSignals: true
466 function onParseFileFinished(filePath, ok, errorMessage) {
467 if (filePath !== pendingBinFile) {
472 QGroundControl.showMessageDialog(logViewerPage, qsTr("Log Viewer"), errorMessage)
478 rebuildGroupedFields()
479 applyParameterFilter()
480 logViewerController.clearSelection()
487 const lowerPath = filePath.toLowerCase()
488 if (lowerPath.endsWith(".ulg")) {
489 logViewerController.openULogFile(filePath)
491 logViewerController.openBinLog(filePath)
498 LogReplayLinkController {
503 Layout.fillWidth: true
504 spacing: ScreenTools.defaultFontPixelWidth
507 text: qsTr("Open .bin")
509 openDialog.nameFilters = ["DataFlash Logs (*.bin *.BIN *.log *.LOG)"]
510 openDialog.openForLoad()
515 text: qsTr("Open .ulg")
517 openDialog.nameFilters = ["PX4 ULog Files (*.ulg *.ULG)"]
518 openDialog.openForLoad()
523 text: qsTr("Open .tlog")
525 const activeVehicle = QGroundControl.multiVehicleManager.activeVehicle
526 if (activeVehicle && !activeVehicle.isOfflineEditingVehicle) {
527 QGroundControl.showMessageDialog(logViewerPage, qsTr("Log Viewer"), qsTr("Close active vehicle connections before starting telemetry replay."))
530 openDialog.nameFilters = ["Telemetry Logs (*.tlog *.TLOG)"]
531 openDialog.openForLoad()
537 enabled: logViewerController.hasLoadedLog
539 clearLoadedLogState(true)
544 Layout.fillWidth: true
545 elide: Text.ElideMiddle
546 text: logViewerController.hasLoadedLog ? logViewerController.currentLogPath : qsTr("No log selected")
551 Layout.fillWidth: true
552 visible: logViewerController.sourceType === LogViewerController.TLog && replayController.link
553 spacing: ScreenTools.defaultFontPixelWidth
556 text: replayController.isPlaying ? qsTr("Pause") : qsTr("Play")
557 onClicked: replayController.isPlaying = !replayController.isPlaying
565 ListElement { text: "0.1x"; value: 0.1 }
566 ListElement { text: "0.25x"; value: 0.25 }
567 ListElement { text: "0.5x"; value: 0.5 }
568 ListElement { text: "1x"; value: 1.0 }
569 ListElement { text: "2x"; value: 2.0 }
570 ListElement { text: "5x"; value: 5.0 }
571 ListElement { text: "10x"; value: 10.0 }
574 onActivated: (index) => replayController.playbackSpeed = model.get(index).value
577 QGCLabel { text: replayController.playheadTime }
581 Layout.fillWidth: true
585 property bool _internalUpdate: false
588 target: replayController
589 function onPercentCompleteChanged(percentComplete) {
590 replaySlider._internalUpdate = true
591 replaySlider.value = percentComplete
592 replaySlider._internalUpdate = false
597 if (!_internalUpdate) {
598 replayController.percentComplete = value
603 QGCLabel { text: replayController.totalTime }
607 Layout.fillWidth: true
608 Layout.fillHeight: true
609 spacing: ScreenTools.defaultFontPixelWidth
612 Layout.preferredWidth: availableWidth * 0.25
613 Layout.fillHeight: true
614 color: qgcPal.windowShade
615 radius: ScreenTools.defaultFontPixelWidth * 0.5
619 anchors.margins: ScreenTools.defaultFontPixelWidth
620 spacing: ScreenTools.defaultFontPixelHeight * 0.5
623 text: qsTr("Messages / Parameters")
628 Layout.fillWidth: true
629 wrapMode: Text.WordWrap
631 text: (isFirmwareLog)
632 ? qsTr("DataFlash message and parameter browser will appear here.")
633 : qsTr("For telemetry replay, MAVLink message and field selection is available through the Inspector integration.")
637 visible: isFirmwareLog
638 text: qsTr("Fields: %1 Parameters: %2 Events: %3")
639 .arg(logParser.plottableFields.length)
640 .arg(logParser.parameters.length)
641 .arg(logParser.events.length)
645 visible: isFirmwareLog
646 text: qsTr("Detected vehicle type: %1")
647 .arg(logParser.detectedVehicleType.length > 0
648 ? logParser.detectedVehicleType
653 Layout.fillWidth: true
654 visible: isFirmwareLog
655 spacing: ScreenTools.defaultFontPixelWidth * 0.5
658 text: qsTr("Fields (click to plot)")
664 Layout.fillWidth: true
665 textColor: qgcPal.textFieldText
666 placeholderTextColor: Qt.rgba(qgcPal.textFieldText.r, qgcPal.textFieldText.g, qgcPal.textFieldText.b, 0.7)
667 placeholderText: qsTr("Search fields")
669 fieldSearchText = text
670 if (text.trim().length === 0) {
671 fieldSearchTimer.stop()
674 fieldSearchTimer.restart()
678 fieldSearchText = text
679 fieldSearchTimer.stop()
685 text: qsTr("Clear Selected")
686 horizontalAlignment: Text.AlignHCenter
687 Layout.preferredHeight: fieldSearchField.implicitHeight
688 Layout.minimumHeight: fieldSearchField.implicitHeight
691 enabled: logViewerController.selectedFields.length > 0
693 logViewerController.clearSelection()
702 Layout.fillWidth: true
703 Layout.preferredHeight: parent.height * 0.35
704 visible: isFirmwareLog
710 model: filteredFieldRows
711 spacing: ScreenTools.defaultFontPixelHeight * 0.15
713 ScrollBar.vertical: ScrollBar { }
716 width: fieldsListView.width
717 height: (modelData.rowType === "group")
718 ? (groupRect.implicitHeight + (ScreenTools.defaultFontPixelHeight * 0.1))
719 : (fieldRow.implicitHeight + (ScreenTools.defaultFontPixelHeight * 0.1))
723 visible: modelData.rowType === "group"
725 implicitHeight: groupLabel.implicitHeight + (ScreenTools.defaultFontPixelHeight * 0.35)
726 color: qgcPal.windowShadeDark
727 radius: ScreenTools.defaultFontPixelWidth * 0.25
731 anchors.leftMargin: ScreenTools.defaultFontPixelWidth * 0.2
732 anchors.verticalCenter: parent.verticalCenter
733 spacing: ScreenTools.defaultFontPixelWidth * 0.3
735 QGCLabel { text: (String(fieldSearchText).trim().length > 0) ? "▼" : (isGroupExpanded(modelData.group) ? "▼" : "▶") }
736 QGCLabel { id: groupLabel; text: modelData.group; font.bold: true }
741 onClicked: toggleGroupExpanded(modelData.group)
747 visible: modelData.rowType === "field"
749 height: Math.max(fieldNameLabel.implicitHeight, fieldCheckBox.implicitHeight)
750 spacing: ScreenTools.defaultFontPixelWidth * 0.25
754 anchors.verticalCenter: parent.verticalCenter
755 onClicked: logViewerController.setFieldSelected(modelData.fullName, checked)
756 checked: logViewerController.selectedFields.indexOf(modelData.fullName) !== -1
761 anchors.verticalCenter: parent.verticalCenter
762 width: Math.max(0, parent.width - (ScreenTools.defaultFontPixelWidth * 3))
763 height: Math.max(implicitHeight, ScreenTools.defaultFontPixelHeight * 1.2)
764 wrapMode: Text.WordWrap
766 verticalAlignment: Text.AlignVCenter
767 text: modelData.shortName ? String(modelData.shortName) : ""
768 color: isFieldSelected(modelData.fullName) ? fieldColor(modelData.fullName) : qgcPal.text
776 Layout.fillWidth: true
777 Layout.preferredHeight: 1
778 color: qgcPal.windowShadeDark
779 visible: isFirmwareLog
784 Layout.fillWidth: true
785 visible: isFirmwareLog
788 text: qsTr("Parameters")
793 text: qsTr("Messages")
799 id: parameterSearchField
800 Layout.fillWidth: true
801 visible: isFirmwareLog && dataTabBar.currentIndex === 0
802 textColor: qgcPal.textFieldText
803 placeholderTextColor: Qt.rgba(qgcPal.textFieldText.r, qgcPal.textFieldText.g, qgcPal.textFieldText.b, 0.7)
804 placeholderText: qsTr("Search parameters")
806 parameterSearchText = text
807 if (text.trim().length === 0) {
808 parameterSearchTimer.stop()
809 applyParameterFilter()
811 parameterSearchTimer.restart()
815 parameterSearchText = text
816 parameterSearchTimer.stop()
817 applyParameterFilter()
822 Layout.fillWidth: true
823 Layout.fillHeight: true
824 Layout.minimumHeight: 0
825 visible: isFirmwareLog && dataTabBar.currentIndex === 0
829 id: parametersListView
831 model: filteredParameters
832 spacing: ScreenTools.defaultFontPixelHeight * 0.2
834 ScrollBar.vertical: ScrollBar { }
837 width: ListView.view.width
838 wrapMode: Text.WordWrap
840 text: modelData.name + " = " + modelData.value
846 Layout.fillWidth: true
847 Layout.fillHeight: true
848 Layout.minimumHeight: 0
849 visible: isFirmwareLog && dataTabBar.currentIndex === 1
855 model: logParser.messages
856 spacing: ScreenTools.defaultFontPixelHeight * 0.2
858 ScrollBar.vertical: ScrollBar { }
861 width: ListView.view.width
862 wrapMode: Text.WordWrap
865 const t = Number(modelData.time)
866 const prefix = isNaN(t) || t < 0 ? "" : ("[" + t.toFixed(3) + "s] ")
867 return prefix + String(modelData.text)
877 Layout.fillWidth: true
878 Layout.fillHeight: true
879 color: qgcPal.windowShadeDark
880 radius: ScreenTools.defaultFontPixelWidth * 0.5
884 anchors.margins: ScreenTools.defaultFontPixelWidth
885 spacing: ScreenTools.defaultFontPixelHeight * 0.5
888 text: qsTr("Charts and Timeline")
893 Layout.fillWidth: true
894 wrapMode: Text.WordWrap
896 text: qsTr("Multi-series charts, event markers, and timeline controls are shown here.")
897 visible: logViewerController.sourceType !== LogViewerController.TLog
902 Layout.fillWidth: true
903 Layout.fillHeight: true
904 Layout.preferredHeight: parent.height * 0.72
905 visible: isFirmwareLog
916 colorScheme: qgcPal.globalTheme === QGCPalette.Light ? GraphsTheme.ColorScheme.Light : GraphsTheme.ColorScheme.Dark
917 backgroundColor: qgcPal.windowShadeDark
918 backgroundVisible: true
919 plotAreaBackgroundColor: qgcPal.windowShadeDark
920 grid.mainColor: Qt.rgba(qgcPal.text.r, qgcPal.text.g, qgcPal.text.b, 0.25)
921 grid.subColor: Qt.rgba(qgcPal.text.r, qgcPal.text.g, qgcPal.text.b, 0.12)
923 labelBackgroundVisible: false
924 labelTextColor: qgcPal.text
925 axisXLabelFont.family: ScreenTools.fixedFontFamily
926 axisXLabelFont.pointSize: ScreenTools.smallFontPointSize
927 axisYLabelFont.family: ScreenTools.fixedFontFamily
928 axisYLabelFont.pointSize: ScreenTools.smallFontPointSize
933 titleText: qsTr("Time (s)")
940 titleText: qsTr("Value")
947 id: zoomSelectionRect
949 color: Qt.rgba(1, 1, 1, 0.2)
950 border.color: qgcPal.buttonHighlight
958 enabled: isFirmwareLog && (binXAxis.max > binXAxis.min)
960 acceptedButtons: Qt.LeftButton | Qt.RightButton
963 property real _dragStartX: 0
965 onPressed: (mouse) => {
966 if (mouse.button === Qt.RightButton) {
970 _dragStartX = mouse.x
971 zoomSelectionRect.x = mouse.x
972 zoomSelectionRect.y = 0
973 zoomSelectionRect.width = 0
974 zoomSelectionRect.height = height
975 zoomSelectionRect.visible = true
976 updateCursorInfo(mouse.x, mouse.y, width, height)
979 onPositionChanged: (mouse) => {
980 if (pressed && zoomSelectionRect.visible) {
981 const left = Math.min(_dragStartX, mouse.x)
982 const right = Math.max(_dragStartX, mouse.x)
983 zoomSelectionRect.x = left
984 zoomSelectionRect.width = Math.max(0, right - left)
989 updateCursorInfo(mouse.x, mouse.y, width, height)
994 onReleased: (mouse) => {
995 if (!zoomSelectionRect.visible) {
999 const dragWidth = zoomSelectionRect.width
1000 zoomSelectionRect.visible = false
1001 // Treat drags narrower than half a character width as accidental clicks, not zoom gestures.
1002 if (dragWidth < ScreenTools.defaultFontPixelWidth * 0.5) {
1006 const leftX = _pixelToAxisX(zoomSelectionRect.x)
1007 const rightX = _pixelToAxisX(zoomSelectionRect.x + zoomSelectionRect.width)
1008 applyZoomRange(Math.min(leftX, rightX), Math.max(leftX, rightX))
1009 updateCursorInfo(mouse.x, mouse.y, width, height)
1013 cursorVisible = false
1018 visible: cursorVisible && (isFirmwareLog)
1020 y: binChart.plotArea.y
1022 height: binChart.plotArea.height
1023 color: qgcPal.buttonHighlight
1029 visible: cursorVisible && cursorRows.length > 0 && (isFirmwareLog)
1030 x: Math.max(0, Math.min(chartContainer.width - width, cursorPixelX + ScreenTools.defaultFontPixelWidth))
1032 width: ScreenTools.defaultFontPixelWidth * 30
1033 color: qgcPal.windowShade
1034 border.color: qgcPal.windowShadeDark
1035 radius: ScreenTools.defaultFontPixelWidth * 0.3
1037 implicitHeight: cursorColumn.implicitHeight + (ScreenTools.defaultFontPixelHeight * 0.6)
1041 anchors.fill: parent
1042 anchors.margins: ScreenTools.defaultFontPixelHeight * 0.3
1043 spacing: ScreenTools.defaultFontPixelHeight * 0.2
1046 text: qsTr("t=%1 s").arg(cursorXValue.toFixed(3))
1051 visible: cursorModeName.length > 0
1052 text: qsTr("Mode: %1").arg(cursorModeName)
1054 color: cursorModeName.length > 0 ? modeColor(cursorModeName) : qgcPal.text
1061 spacing: ScreenTools.defaultFontPixelWidth * 0.2
1064 width: ScreenTools.defaultFontPixelWidth * 0.8
1065 height: ScreenTools.defaultFontPixelHeight * 0.6
1066 color: modelData.color
1070 width: cursorPopup.width - (ScreenTools.defaultFontPixelWidth * 4)
1071 elide: Text.ElideMiddle
1072 text: modelData.name + ": " + Number(modelData.value).toFixed(3)
1078 model: cursorEventRows
1081 spacing: ScreenTools.defaultFontPixelWidth * 0.2
1084 width: ScreenTools.defaultFontPixelWidth * 0.8
1085 height: ScreenTools.defaultFontPixelHeight * 0.6
1086 color: modelData.color
1090 width: cursorPopup.width - (ScreenTools.defaultFontPixelWidth * 4)
1091 wrapMode: Text.WordWrap
1093 text: modelData.text
1102 Layout.fillWidth: true
1103 Layout.preferredHeight: ScreenTools.defaultFontPixelHeight * 0.6
1104 visible: isFirmwareLog
1105 color: qgcPal.windowShade
1108 model: logParser.modeSegments
1111 visible: binXAxis.max > binXAxis.min && modelData.end >= binXAxis.min && modelData.start <= binXAxis.max
1113 height: parent.height
1114 color: modeColor(modelData.mode)
1116 x: Math.max(binChart.plotArea.x,
1117 Math.min(binChart.plotArea.x + binChart.plotArea.width,
1118 binChart.plotArea.x + ((Math.max(modelData.start, binXAxis.min) - binXAxis.min) / (binXAxis.max - binXAxis.min)) * binChart.plotArea.width))
1119 width: Math.max(1, ((Math.min(modelData.end, binXAxis.max) - Math.max(modelData.start, binXAxis.min)) / (binXAxis.max - binXAxis.min)) * binChart.plotArea.width)
1124 model: logParser.dropouts
1127 visible: binXAxis.max > binXAxis.min && modelData.end >= binXAxis.min && modelData.start <= binXAxis.max
1129 height: parent.height
1130 color: Qt.alpha(eventColor("error"), 0.533)
1131 x: Math.max(binChart.plotArea.x,
1132 Math.min(binChart.plotArea.x + binChart.plotArea.width,
1133 binChart.plotArea.x + ((Math.max(modelData.start, binXAxis.min) - binXAxis.min) / (binXAxis.max - binXAxis.min)) * binChart.plotArea.width))
1134 width: Math.max(2, ((Math.min(modelData.end, binXAxis.max) - Math.max(modelData.start, binXAxis.min)) / (binXAxis.max - binXAxis.min)) * binChart.plotArea.width)
1139 model: logParser.events
1143 height: parent.height
1144 visible: binXAxis.max > binXAxis.min
1145 color: eventColor(modelData.type)
1146 x: Math.max(binChart.plotArea.x,
1147 Math.min((binChart.plotArea.x + binChart.plotArea.width) - width,
1148 binChart.plotArea.x + ((modelData.time - binXAxis.min) / (binXAxis.max - binXAxis.min)) * binChart.plotArea.width))
1154 Layout.fillWidth: true
1155 visible: isFirmwareLog && modeLegendEntries().length > 0
1156 Layout.preferredHeight: ScreenTools.defaultFontPixelHeight * 1.1
1157 spacing: ScreenTools.defaultFontPixelWidth
1160 text: qsTr("Modes:")
1165 model: modeLegendEntries()
1168 spacing: ScreenTools.defaultFontPixelWidth * 0.2
1171 width: ScreenTools.defaultFontPixelWidth
1172 height: ScreenTools.defaultFontPixelHeight * 0.6
1173 color: modeColor(modelData)
1184 Layout.fillWidth: true
1185 visible: isFirmwareLog && logViewerController.selectedFields.length > 0
1186 Layout.preferredHeight: ScreenTools.defaultFontPixelHeight * 1.1
1187 spacing: ScreenTools.defaultFontPixelWidth
1190 text: qsTr("Fields:")
1195 model: logViewerController.selectedFields
1198 spacing: ScreenTools.defaultFontPixelWidth * 0.2
1201 width: ScreenTools.defaultFontPixelWidth
1202 height: ScreenTools.defaultFontPixelHeight * 0.6
1203 color: fieldColor(modelData)
1214 Layout.fillWidth: true
1215 visible: isFirmwareLog && logParser.events.length > 0
1216 Layout.preferredHeight: ScreenTools.defaultFontPixelHeight * 1.1
1217 spacing: ScreenTools.defaultFontPixelWidth
1220 text: qsTr("Events:")
1225 model: ["mode", "event", "error", "warning"]
1228 spacing: ScreenTools.defaultFontPixelWidth * 0.2
1230 for (let i = 0; i < logParser.events.length; i++) {
1231 if (logParser.events[i].type === modelData) {
1239 width: ScreenTools.defaultFontPixelWidth
1240 height: ScreenTools.defaultFontPixelHeight * 0.6
1241 color: eventColor(modelData)
1245 text: eventTypeLabel(modelData)
1252 Layout.fillWidth: true
1253 visible: isFirmwareLog
1254 spacing: ScreenTools.defaultFontPixelWidth
1257 Layout.fillWidth: true
1258 text: qsTr("Drag on chart to zoom X-axis. Right click chart to reset zoom.")
1262 text: qsTr("Reset Zoom")
1263 enabled: zoomMinX !== fullMinX || zoomMaxX !== fullMaxX
1264 onClicked: resetZoom()
1269 Layout.fillWidth: true
1270 Layout.fillHeight: true
1271 active: logViewerController.sourceType === LogViewerController.TLog
1272 source: "qrc:/qml/QGroundControl/AnalyzeView/MAVLinkInspector/MAVLinkInspectorPage.qml"
1279 Layout.fillWidth: true
1280 text: logViewerController.statusText
1281 visible: text.length > 0
1286 title: qsTr("Select log file")
1287 folder: QGroundControl.settingsManager.appSettings.logSavePath
1290 onAcceptedForLoad: (file) => {
1291 const fileLower = file.toLowerCase()
1292 if (fileLower.endsWith(".tlog")) {
1293 if (logViewerController.hasLoadedLog) {
1294 clearLoadedLogState(true)
1296 const replayLink = QGroundControl.linkManager.startLogReplay(file)
1298 QGroundControl.showMessageDialog(
1301 qsTr("Failed to start telemetry replay for the selected .tlog file.")
1306 replayController.link = replayLink
1307 logViewerController.openTLog(file)
1316 Layout.fillWidth: true
1317 Layout.fillHeight: true
1319 color: Qt.rgba(0, 0, 0, 0.4)
1323 anchors.centerIn: parent
1324 spacing: ScreenTools.defaultFontPixelHeight * 0.5
1327 anchors.horizontalCenter: parent.horizontalCenter
1332 text: qsTr("Parsing log file...")
1341 onTriggered: _executePendingBinParse()
1345 id: fieldSearchTimer
1348 onTriggered: applyFieldFilter()
1352 id: parameterSearchTimer
1355 onTriggered: applyParameterFilter()