QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
APMServoComponent.qml
Go to the documentation of this file.
1import QtQuick
2import QtQuick.Controls
3import QtQuick.Layouts
4
5import QGroundControl
6import QGroundControl.Controls
7import QGroundControl.FactControls
8
9SetupPage {
10 id: servoPage
11 pageComponent: pageComponent
12 showAdvanced: false
13
14 readonly property int _maxServos: 16
15 readonly property real _margins: ScreenTools.defaultFontPixelHeight * 0.5
16
17 FactPanelController {
18 id: controller
19 }
20
21 ServoOutputMonitorController {
22 id: servoMonitor
23 }
24
25 QGCPalette {
26 id: qgcPal
27 colorGroupEnabled: true
28 }
29
30 // Keep the "Position (us)" bar a stable width so the table doesn't reflow.
31 readonly property int _positionBarWidth: ScreenTools.defaultFontPixelWidth * 10
32
33 function getFact(param) {
34 return controller.parameterExists(-1, param)
35 ? controller.getParameterFact(-1, param, false)
36 : null
37 }
38
39 function servoExists(n) {
40 return controller.parameterExists(-1, "SERVO" + n + "_FUNCTION")
41 }
42
43 Component {
44 id: pageComponent
45
46 Column {
47 width: availableWidth
48 spacing: _margins
49
50 QGCLabel {
51 text: qsTr("Configure ArduPilot servo outputs.")
52 wrapMode: Text.WordWrap
53 width: parent.width
54 }
55
56 QGCGroupBox {
57 title: qsTr("Servo Outputs")
58
59 GridLayout {
60 columns: 7
61 rowSpacing: ScreenTools.defaultFontPixelHeight * 0.3
62 columnSpacing: ScreenTools.defaultFontPixelWidth * 2
63
64 // --- Headers (all explicitly in row 0) -----------------
65 QGCLabel { text: ""; Layout.row: 0; Layout.column: 0; Layout.alignment: Qt.AlignHCenter }
66 QGCLabel {
67 text: qsTr("Position")
68 Layout.row: 0
69 Layout.column: 1
70 Layout.alignment: Qt.AlignHCenter
71 }
72 QGCLabel { text: qsTr("Function"); Layout.row: 0; Layout.column: 2; Layout.alignment: Qt.AlignHCenter }
73 QGCLabel { text: qsTr("Min"); Layout.row: 0; Layout.column: 3; Layout.alignment: Qt.AlignHCenter }
74 QGCLabel { text: qsTr("Trim"); Layout.row: 0; Layout.column: 4; Layout.alignment: Qt.AlignHCenter }
75 QGCLabel { text: qsTr("Max"); Layout.row: 0; Layout.column: 5; Layout.alignment: Qt.AlignHCenter }
76 QGCLabel { text: qsTr("Reversed"); Layout.row: 0; Layout.column: 6; Layout.alignment: Qt.AlignHCenter }
77
78 // --- Column 0: Servo number ----------------------------
79 Repeater {
80 model: _maxServos
81
82 QGCLabel {
83 text: index + 1
84 visible: servoExists(index + 1)
85 Layout.row: index + 1
86 Layout.column: 0
87 }
88 }
89
90 // --- Column 1: Position --------------------------------
91 Repeater {
92 id: positionRepeater
93 model: _maxServos
94
95 Item {
96 readonly property int _servoIndex: index + 1
97 readonly property var _minFact: getFact("SERVO" + _servoIndex + "_MIN")
98 readonly property var _maxFact: getFact("SERVO" + _servoIndex + "_MAX")
99
100 property int pwmValue: servoMonitor.servoValue(index)
101 readonly property double _rawValue: pwmValue >= 0 ? pwmValue : NaN
102 readonly property double _minValue: _minFact ? _minFact.value : NaN
103 readonly property double _maxValue: _maxFact ? _maxFact.value : NaN
104
105 readonly property double _range: _maxValue - _minValue
106 readonly property double _ratio: (_range > 0 && !isNaN(_rawValue) && !isNaN(_minValue))
107 ? Math.max(0, Math.min(1, (_rawValue - _minValue) / _range))
108 : 0
109
110 height: ScreenTools.defaultFontPixelHeight * 0.95
111 Layout.preferredWidth: _positionBarWidth
112 Layout.fillWidth: false
113 width: _positionBarWidth
114 visible: servoExists(_servoIndex)
115 Layout.row: index + 1
116 Layout.column: 1
117
118 readonly property color _trackColor: qgcPal.colorGrey
119 // Use a themed accent so the fill is always clearly distinct from the track
120 readonly property color _progressColor: qgcPal.colorGreen
121
122 readonly property bool _hasValidValue: pwmValue >= 0 && !isNaN(_rawValue) && !isNaN(_minValue) && !isNaN(_maxValue) && _range > 0
123 // Use the delegate width (cell width), not `parent.width` (Repeater/GridLayout width).
124 readonly property real _progressWidth: _hasValidValue ? Math.max(0, _ratio * width) : 0
125
126 // Keep the delegate's implicit width stable so GridLayout doesn't reflow
127 // based on changing label text lengths.
128 implicitWidth: 0
129
130 Rectangle {
131 anchors.fill: parent
132 id: track
133 color: _trackColor
134 opacity: 0.45
135 border.width: 1
136 border.color: qgcPal.text
137 radius: ScreenTools.defaultBorderRadius
138 }
139
140 Rectangle {
141 anchors.top: parent.top
142 anchors.bottom: parent.bottom
143 anchors.left: parent.left
144 width: _progressWidth
145 color: _progressColor
146 radius: ScreenTools.defaultBorderRadius
147 }
148
149 QGCLabel {
150 id: valueLabel
151 z: 1
152 anchors.centerIn: parent
153 horizontalAlignment: Text.AlignHCenter
154 verticalAlignment: Text.AlignVCenter
155 text: _hasValidValue ? Math.round(_rawValue) : "-"
156 font.bold: true
157 color: qgcPal.text
158 width: parent.width
159 }
160 }
161 }
162
163 Connections {
164 target: servoMonitor
165 function onServoValueChanged(servo, pwmValue) {
166 const item = positionRepeater.itemAt(servo)
167 if (item) {
168 item.pwmValue = pwmValue
169 }
170 }
171 }
172
173 // --- Column 2: Function --------------------------------
174 Repeater {
175 model: _maxServos
176
177 FactComboBox {
178 fact: getFact("SERVO" + (index + 1) + "_FUNCTION")
179 indexModel: false
180 sizeToContents: true
181 visible: servoExists(index + 1)
182 Layout.row: index + 1
183 Layout.column: 2
184 }
185 }
186
187 // --- Column 3: Min ---------------------------------------
188 Repeater {
189 model: _maxServos
190
191 RowLayout {
192 spacing: ScreenTools.defaultFontPixelWidth / 2
193 visible: servoExists(index + 1)
194 Layout.row: index + 1
195 Layout.column: 3
196
197 readonly property var spFact: getFact("SERVO" + (index + 1) + "_MIN")
198 property int _repeatDir: 0
199
200 Timer {
201 id: repeatInitial
202 interval: 350
203 repeat: false
204 running: false
205 onTriggered: repeatTimer.start()
206 }
207
208 Timer {
209 id: repeatTimer
210 interval: 80
211 repeat: true
212 running: false
213 onTriggered: if (spFact && _repeatDir !== 0) { spFact.value += _repeatDir }
214 }
215
216 QGCButton {
217 text: "-"
218 leftPadding: 0
219 rightPadding: 0
220 topPadding: 0
221 bottomPadding: 0
222 Layout.preferredWidth: ScreenTools.implicitTextFieldHeight
223 Layout.preferredHeight: ScreenTools.implicitTextFieldHeight
224 onPressed: {
225 if (!spFact) {
226 _repeatDir = 0
227 return
228 }
229 _repeatDir = -1
230 spFact.value -= 1
231 repeatInitial.start()
232 }
233 onReleased: {
234 _repeatDir = 0
235 repeatInitial.stop()
236 repeatTimer.stop()
237 }
238 }
239 FactTextField { fact: spFact; showUnits: false; Layout.fillWidth: true }
240 QGCButton {
241 text: "+"
242 leftPadding: 0
243 rightPadding: 0
244 topPadding: 0
245 bottomPadding: 0
246 Layout.preferredWidth: ScreenTools.implicitTextFieldHeight
247 Layout.preferredHeight: ScreenTools.implicitTextFieldHeight
248 onPressed: {
249 if (!spFact) {
250 _repeatDir = 0
251 return
252 }
253 _repeatDir = 1
254 spFact.value += 1
255 repeatInitial.start()
256 }
257 onReleased: {
258 _repeatDir = 0
259 repeatInitial.stop()
260 repeatTimer.stop()
261 }
262 }
263 }
264 }
265
266 // --- Column 4: Trim --------------------------------------
267 Repeater {
268 model: _maxServos
269
270 RowLayout {
271 spacing: ScreenTools.defaultFontPixelWidth / 2
272 visible: servoExists(index + 1)
273 Layout.row: index + 1
274 Layout.column: 4
275
276 readonly property var spFact: getFact("SERVO" + (index + 1) + "_TRIM")
277 property int _repeatDir: 0
278
279 Timer {
280 id: repeatInitialTrim
281 interval: 350
282 repeat: false
283 running: false
284 onTriggered: repeatTimerTrim.start()
285 }
286
287 Timer {
288 id: repeatTimerTrim
289 interval: 80
290 repeat: true
291 running: false
292 onTriggered: if (spFact && _repeatDir !== 0) { spFact.value += _repeatDir }
293 }
294
295 QGCButton {
296 text: "-"
297 leftPadding: 0
298 rightPadding: 0
299 topPadding: 0
300 bottomPadding: 0
301 Layout.preferredWidth: ScreenTools.implicitTextFieldHeight
302 Layout.preferredHeight: ScreenTools.implicitTextFieldHeight
303 onPressed: {
304 if (!spFact) {
305 _repeatDir = 0
306 return
307 }
308 _repeatDir = -1
309 spFact.value -= 1
310 repeatInitialTrim.start()
311 }
312 onReleased: {
313 _repeatDir = 0
314 repeatInitialTrim.stop()
315 repeatTimerTrim.stop()
316 }
317 }
318 FactTextField { fact: spFact; showUnits: false; Layout.fillWidth: true }
319 QGCButton {
320 text: "+"
321 leftPadding: 0
322 rightPadding: 0
323 topPadding: 0
324 bottomPadding: 0
325 Layout.preferredWidth: ScreenTools.implicitTextFieldHeight
326 Layout.preferredHeight: ScreenTools.implicitTextFieldHeight
327 onPressed: {
328 if (!spFact) {
329 _repeatDir = 0
330 return
331 }
332 _repeatDir = 1
333 spFact.value += 1
334 repeatInitialTrim.start()
335 }
336 onReleased: {
337 _repeatDir = 0
338 repeatInitialTrim.stop()
339 repeatTimerTrim.stop()
340 }
341 }
342 }
343 }
344
345 // --- Column 5: Max ---------------------------------------
346 Repeater {
347 model: _maxServos
348
349 RowLayout {
350 spacing: ScreenTools.defaultFontPixelWidth / 2
351 visible: servoExists(index + 1)
352 Layout.row: index + 1
353 Layout.column: 5
354
355 readonly property var spFact: getFact("SERVO" + (index + 1) + "_MAX")
356 property int _repeatDir: 0
357
358 Timer {
359 id: repeatInitialMax
360 interval: 350
361 repeat: false
362 running: false
363 onTriggered: repeatTimerMax.start()
364 }
365
366 Timer {
367 id: repeatTimerMax
368 interval: 80
369 repeat: true
370 running: false
371 onTriggered: if (spFact && _repeatDir !== 0) { spFact.value += _repeatDir }
372 }
373
374 QGCButton {
375 text: "-"
376 leftPadding: 0
377 rightPadding: 0
378 topPadding: 0
379 bottomPadding: 0
380 Layout.preferredWidth: ScreenTools.implicitTextFieldHeight
381 Layout.preferredHeight: ScreenTools.implicitTextFieldHeight
382 onPressed: {
383 if (!spFact) {
384 _repeatDir = 0
385 return
386 }
387 _repeatDir = -1
388 spFact.value -= 1
389 repeatInitialMax.start()
390 }
391 onReleased: {
392 _repeatDir = 0
393 repeatInitialMax.stop()
394 repeatTimerMax.stop()
395 }
396 }
397 FactTextField { fact: spFact; showUnits: false; Layout.fillWidth: true }
398 QGCButton {
399 text: "+"
400 leftPadding: 0
401 rightPadding: 0
402 topPadding: 0
403 bottomPadding: 0
404 Layout.preferredWidth: ScreenTools.implicitTextFieldHeight
405 Layout.preferredHeight: ScreenTools.implicitTextFieldHeight
406 onPressed: {
407 if (!spFact) {
408 _repeatDir = 0
409 return
410 }
411 _repeatDir = 1
412 spFact.value += 1
413 repeatInitialMax.start()
414 }
415 onReleased: {
416 _repeatDir = 0
417 repeatInitialMax.stop()
418 repeatTimerMax.stop()
419 }
420 }
421 }
422 }
423
424 // --- Column 6: Reversed ---------------------------------
425 Repeater {
426 model: _maxServos
427
428 FactCheckBox {
429 fact: getFact("SERVO" + (index + 1) + "_REVERSED")
430 visible: servoExists(index + 1)
431 Layout.row: index + 1
432 Layout.column: 6
433 Layout.alignment: Qt.AlignHCenter
434 }
435 }
436 }
437 }
438 }
439 }
440}