QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
PIDTuning.qml
Go to the documentation of this file.
1import QtQuick
2import QtQuick.Controls
3import QtGraphs
4import QtQuick.Layouts
5
6import QGroundControl
7import QGroundControl.Controls
8import QGroundControl.FactControls
9
10RowLayout {
11 spacing: _margins
12
13 QGCPalette { id: qgcPal }
14
15 property real availableHeight
16 property real availableWidth
17 property var axis
18 property string unit
19 property string title
20 property var tuningMode
21 property double chartDisplaySec: 8 // number of seconds to display
22 property bool showAutoModeChange: false
23 property bool showAutoTuning: false
24 property bool useAutoTuning: false
25
26 property real _margins: ScreenTools.defaultFontPixelHeight / 2
27 property int _currentAxis: 0
28 property var _xAxis: xAxis
29 property var _yAxis: yAxis
30 property int _msecs: 0
31 property double _last_t: 0
32 property var _savedTuningParamValues: [ ]
33
34 readonly property int _tickSeparation: 5
35 readonly property int _maxTickSections: 10
36
37 property string _chartTitle: ""
38 readonly property var _seriesColors: ["#21be2b", "#c62828", "#1565c0", "#f9a825", "#6a1b9a", "#00838f"]
39 property var _legendModel: []
40
41 Component {
42 id: lineSeriesComponent
43 LineSeries { }
44 }
45
46 function adjustYAxisMin(yAxis, newValue) {
47 var newMin = Math.min(yAxis.min, newValue)
48 if (newMin % 5 != 0) {
49 newMin -= 5
50 newMin = Math.floor(newMin / _tickSeparation) * _tickSeparation
51 }
52 yAxis.min = newMin
53 }
54
55 function adjustYAxisMax(yAxis, newValue) {
56 var newMax = Math.max(yAxis.max, newValue)
57 if (newMax % 5 != 0) {
58 newMax += 5
59 newMax = Math.floor(newMax / _tickSeparation) * _tickSeparation
60 }
61 yAxis.max = newMax
62 }
63
64 function resetGraphs() {
65 for (var i = 0; i < chart.seriesList.length; ++i) {
66 chart.seriesList[i].clear()
67 }
68 _xAxis.min = 0
69 _xAxis.max = 0
70 _yAxis.min = 0
71 _yAxis.max = 0
72 _msecs = 0
73 _last_t = 0
74 }
75
76 // Save the current set of tuning values so we can reset to them
77 function saveTuningParamValues() {
78 _savedTuningParamValues = [ ]
79 for (var i=0; i<axis[_currentAxis].params.count; i++) {
80 var currentTuneParam = controller.getParameterFact(-1, axis[_currentAxis].params.get(i).param)
81 _savedTuningParamValues.push(currentTuneParam.valueString)
82 }
83 savedRepeater.model = _savedTuningParamValues
84 }
85
86 function resetToSavedTuningParamValues() {
87 for (var i=0; i<axis[_currentAxis].params.count; i++) {
88 var currentTuneParam = controller.getParameterFact(-1,
89 axis[_currentAxis].params.get(i).param)
90 currentTuneParam.value = _savedTuningParamValues[i]
91 }
92 }
93
94 function axisIndexChanged() {
95 while (chart.seriesList.length > 0) {
96 // Do not call s.destroy() here. QGraphsView holds an internal
97 // pointer to the series that is only cleared during the next
98 // updatePolish() pass. Destroying the series before that pass
99 // causes a SIGSEGV in QGraphsView::updatePolish(). The series is
100 // parented to chart so it is freed when the chart is destroyed.
101 // GPU/graph resources are released by removeSeries().
102 chart.removeSeries(chart.seriesList[0])
103 }
104 var legendItems = []
105 axis[_currentAxis].plot.forEach(function(e, idx) {
106 var color = _seriesColors[idx % _seriesColors.length]
107 var series = lineSeriesComponent.createObject(chart, {name: e.name, color: color})
108 chart.addSeries(series)
109 legendItems.push({name: e.name, color: color})
110 })
111 _legendModel = legendItems
112 var chartTitle = axis[_currentAxis].plotTitle
113 if (chartTitle == null)
114 chartTitle = axis[_currentAxis].name
115 _chartTitle = chartTitle + " " + title
116 saveTuningParamValues()
117 resetGraphs()
118 }
119
120 Component.onCompleted: {
121 axisIndexChanged()
122 globals.activeVehicle.setPIDTuningTelemetryMode(tuningMode)
123 saveTuningParamValues()
124 }
125
126 Component.onDestruction: globals.activeVehicle.setPIDTuningTelemetryMode(Vehicle.ModeDisabled)
127 on_CurrentAxisChanged: axisIndexChanged()
128
129 Timer {
130 id: dataTimer
131 interval: 10
132 running: true
133 repeat: true
134
135 onTriggered: {
136 _xAxis.max = _msecs / 1000
137 _xAxis.min = _msecs / 1000 - chartDisplaySec
138
139 var firstPoint = _msecs == 0
140
141 var len = axis[_currentAxis].plot.length
142 for (var i = 0; i < len; ++i) {
143 var value = axis[_currentAxis].plot[i].value
144 if (!isNaN(value)) {
145 chart.seriesList[i].append(_msecs/1000, value)
146 if (firstPoint) {
147 _yAxis.min = value
148 _yAxis.max = value
149 } else {
150 adjustYAxisMin(_yAxis, value)
151 adjustYAxisMax(_yAxis, value)
152 }
153 // limit history
154 var minSec = _msecs/1000 - 3*60
155 while (chart.seriesList[i].count > 0 && chart.seriesList[i].at(0).x < minSec) {
156 chart.seriesList[i].remove(0)
157 }
158 }
159 }
160
161 var t = new Date().getTime() // in ms
162 if (_last_t > 0)
163 _msecs += t-_last_t
164 _last_t = t
165 }
166
167 property int _maxPointCount: 10000 / interval
168 }
169
170 Column {
171 id: leftPanel
172 Layout.fillWidth: true
173 Layout.alignment: Qt.AlignTop
174 spacing: ScreenTools.defaultFontPixelHeight / 4
175 clip: true // chart has redraw problems
176
177 QGCLabel {
178 id: chartTitleLabel
179 text: _chartTitle
180 font.pointSize: ScreenTools.defaultFontPointSize
181 font.family: ScreenTools.normalFontFamily
182 anchors.horizontalCenter: parent.horizontalCenter
183 }
184
185 GraphsView {
186 id: chart
187 width: Math.max(_minChartWidth, availableWidth - rightPanel.width - parent.spacing - _margins)
188 height: Math.max(_minChartHeight, availableHeight - leftPanelBottomColumn.height - chartTitleLabel.height - legendRow.height - parent.spacing * 3 - _margins)
189
190 property real _minChartWidth: ScreenTools.defaultFontPixelWidth * 40
191 property real _minChartHeight: ScreenTools.defaultFontPixelHeight * 15
192
193 theme: GraphsTheme {
194 colorScheme: qgcPal.globalTheme === QGCPalette.Light ? GraphsTheme.ColorScheme.Light : GraphsTheme.ColorScheme.Dark
195 plotAreaBackgroundColor: qgcPal.window
196 grid.mainColor: Qt.rgba(qgcPal.text.r, qgcPal.text.g, qgcPal.text.b, 0.5)
197 grid.subColor: Qt.rgba(qgcPal.text.r, qgcPal.text.g, qgcPal.text.b, 0.3)
198 grid.mainWidth: 1
199 labelBackgroundVisible: false
200 labelTextColor: qgcPal.text
201 }
202
203 axisX: ValueAxis {
204 id: xAxis
205 min: 0
206 max: 0
207 labelFormat: "%.1f"
208 titleText: ScreenTools.isShortScreen ? "" : qsTr("sec")
209 titleFont.pointSize: ScreenTools.defaultFontPointSize
210 titleFont.family: ScreenTools.normalFontFamily
211 }
212
213 axisY: ValueAxis {
214 id: yAxis
215 min: 0
216 max: 10
217 titleText: unit
218 tickInterval: _tickSeparation
219 titleFont.pointSize: ScreenTools.defaultFontPointSize
220 titleFont.family: ScreenTools.normalFontFamily
221 }
222
223 // enable mouse dragging
224 MouseArea {
225 property var _startPoint: undefined
226 property double _scaling: 0
227 anchors.fill: parent
228 onPressed: (mouse) => {
229 _startPoint = Qt.point(mouse.x, mouse.y)
230 if (chart.seriesList.length > 0) {
231 var start = chart.seriesList[0].dataPointCoordinatesAt(_startPoint.x, _startPoint.y)
232 var next = chart.seriesList[0].dataPointCoordinatesAt(mouse.x+1, mouse.y+1)
233 _scaling = next.x - start.x
234 }
235 }
236 onWheel: (wheel) => {
237 if (wheel.angleDelta.y > 0)
238 chartDisplaySec /= 1.2
239 else
240 chartDisplaySec *= 1.2
241 _xAxis.min = _xAxis.max - chartDisplaySec
242 }
243 onPositionChanged: (mouse) => {
244 if(_startPoint != undefined) {
245 dataTimer.running = false
246 var cp = Qt.point(mouse.x, mouse.y)
247 var dx = (cp.x - _startPoint.x) * _scaling
248 _startPoint = cp
249 _xAxis.max -= dx
250 _xAxis.min -= dx
251 }
252 }
253
254 onReleased: {
255 _startPoint = undefined
256 }
257 }
258 }
259
260 Row {
261 id: legendRow
262 spacing: ScreenTools.defaultFontPixelWidth
263 anchors.horizontalCenter: parent.horizontalCenter
264
265 Repeater {
266 model: _legendModel
267 Row {
268 spacing: ScreenTools.defaultFontPixelWidth / 2
269 Rectangle {
270 width: ScreenTools.defaultFontPixelHeight
271 height: ScreenTools.defaultFontPixelHeight / 3
272 color: modelData.color
273 anchors.verticalCenter: parent.verticalCenter
274 }
275 QGCLabel {
276 text: modelData.name
277 font.pointSize: ScreenTools.smallFontPointSize
278 }
279 }
280 }
281 }
282
283 Column {
284 id: leftPanelBottomColumn
285 spacing: ScreenTools.defaultFontPixelHeight / 4
286
287 RowLayout {
288 spacing: _margins
289
290 QGCButton {
291 text: qsTr("Clear")
292 onClicked: resetGraphs()
293 }
294
295 QGCButton {
296 text: dataTimer.running ? qsTr("Stop") : qsTr("Start")
297 onClicked: {
298 dataTimer.running = !dataTimer.running
299 _last_t = 0
300 if (showAutoModeChange && autoModeChange.checked) {
301 globals.activeVehicle.flightMode = dataTimer.running ? globals.activeVehicle.stabilizedFlightMode : globals.activeVehicle.pauseFlightMode
302 }
303 }
304 }
305 Connections {
306 target: globals.activeVehicle
307 onArmedChanged: {
308 if (armed && !dataTimer.running) { // start plotting on arming if not already running
309 dataTimer.running = true
310 _last_t = 0
311 }
312 }
313 }
314 }
315
316 QGCCheckBox {
317 visible: showAutoModeChange
318 id: autoModeChange
319 text: qsTr("Automatic Flight Mode Switching")
320 onClicked: {
321 if (checked)
322 dataTimer.running = false
323 }
324 }
325
326 Column {
327 visible: autoModeChange.checked
328 QGCLabel {
329 text: qsTr("Switches to 'Stabilized' when you click Start.")
330 font.pointSize: ScreenTools.smallFontPointSize
331 }
332
333 QGCLabel {
334 text: qsTr("Switches to '%1' when you click Stop.").arg(globals.activeVehicle.pauseFlightMode)
335 font.pointSize: ScreenTools.smallFontPointSize
336 }
337 }
338 }
339 }
340
341 ColumnLayout {
342 id: rightPanel
343 Layout.alignment: Qt.AlignTop
344
345 RowLayout {
346 visible: showAutoTuning
347
348 QGCRadioButton {
349 id: useAutoTuningRadio
350 text: qsTr("Use auto-tuning")
351 checked: useAutoTuning
352 onClicked: useAutoTuning = true
353 }
354 QGCRadioButton {
355 id: useManualTuningRadio
356 text: qsTr("Use manual tuning")
357 checked: !useAutoTuning
358 onClicked: useAutoTuning = false
359 }
360 }
361
362 AutotuneUI {
363 visible: showAutoTuning && useAutoTuningRadio.checked
364 }
365
366 ColumnLayout {
367 visible: !showAutoTuning || useManualTuningRadio.checked
368
369 Column {
370 RowLayout {
371 spacing: _margins
372 visible: axis.length > 1
373
374 QGCLabel { text: qsTr("Select Tuning:") }
375
376 Repeater {
377 model: axis
378 QGCRadioButton {
379 objectName: "pidTuning_axisButton_" + modelData.name.replace(/ /g, "")
380 text: modelData.name
381 checked: index == _currentAxis
382 onClicked: _currentAxis = index
383 }
384 }
385 }
386 }
387
388 // Instantiate all sliders (instead of switching the model), so that
389 // values are not changed unexpectedly if they do not match with a tick value.
390 Repeater {
391 model: axis
392
393 Repeater {
394 id: paramRepeater
395 model: axis[index].params
396
397 property int axisIndex: index
398
399 SettingsGroupLayout {
400 id: tuningGroup
401 heading: title
402 headingDescription: description
403 visible: _currentAxis === paramRepeater.axisIndex
404 Layout.preferredWidth: ScreenTools.defaultFontPixelWidth * 40
405
406 FactSlider {
407 fact: controller.getParameterFact(-1, param)
408 from: min
409 to: max
410 majorTickStepSize: step
411 Layout.fillWidth: true
412 }
413 }
414 }
415 }
416
417 Column {
418 QGCLabel { text: qsTr("Clipboard Values:") }
419
420 GridLayout {
421 rows: savedRepeater.model.length
422 flow: GridLayout.TopToBottom
423 rowSpacing: 0
424 columnSpacing: _margins
425
426 Repeater {
427 model: axis[_currentAxis].params
428
429 QGCLabel { text: param }
430 }
431
432 Repeater {
433 id: savedRepeater
434
435 QGCLabel { text: modelData }
436 }
437 }
438 }
439
440 RowLayout {
441 spacing: _margins
442
443 QGCButton {
444 text: qsTr("Save To Clipboard")
445 onClicked: saveTuningParamValues()
446 }
447
448 QGCButton {
449 text: qsTr("Restore From Clipboard")
450 onClicked: resetToSavedTuningParamValues()
451 }
452 }
453 }
454 }
455
456} // RowLayout