QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
BatteryIndicator.qml
Go to the documentation of this file.
1import QtQuick
2import QtQuick.Layouts
3
4import QGroundControl
5import QGroundControl.Controls
6import QGroundControl.FactControls
7
8//-------------------------------------------------------------------------
9//-- Battery Indicator
10Item {
11 id: control
12 anchors.top: parent.top
13 anchors.bottom: parent.bottom
14 width: batteryIndicatorRow.width
15
16 property bool showIndicator: _activeVehicle && _activeVehicle.batteries.count > 0
17 property bool waitForParameters: true // UI won't show until parameters are ready
18 property Component expandedPageComponent
19
20 property var _activeVehicle: QGroundControl.multiVehicleManager.activeVehicle
21 property var _batterySettings: QGroundControl.settingsManager.batteryIndicatorSettings
22 property Fact _indicatorDisplay: _batterySettings.valueDisplay
23 property bool _showPercentage: _indicatorDisplay.rawValue === 0
24 property bool _showVoltage: _indicatorDisplay.rawValue === 1
25 property bool _showBoth: _indicatorDisplay.rawValue === 2
26 property int _lowestBatteryId: -1 // -1: show all batteries, otherwise show only battery with this id
27
28 // Properties to hold the thresholds
29 property int threshold1: _batterySettings.threshold1.rawValue
30 property int threshold2: _batterySettings.threshold2.rawValue
31
32 function _recalcLowestBatteryIdFromVoltage() {
33 if (_activeVehicle) {
34 // If there is only one battery then it is the lowest
35 if (_activeVehicle.batteries.count === 1) {
36 _lowestBatteryId = _activeVehicle.batteries.get(0).id.rawValue
37 return
38 }
39
40 // If we have valid voltage for all batteries we use that to determine lowest battery
41 let allHaveVoltage = true
42 for (var i = 0; i < _activeVehicle.batteries.count; i++) {
43 let battery = _activeVehicle.batteries.get(i)
44 if (isNaN(battery.voltage.rawValue)) {
45 allHaveVoltage = false
46 break
47 }
48 }
49 if (allHaveVoltage) {
50 let lowestBattery = _activeVehicle.batteries.get(0)
51 let lowestBatteryId = lowestBattery.id.rawValue
52 for (var i = 1; i < _activeVehicle.batteries.count; i++) {
53 let battery = _activeVehicle.batteries.get(i)
54 if (battery.voltage.rawValue < lowestBattery.voltage.rawValue) {
55 lowestBattery = battery
56 lowestBatteryId = battery.id.rawValue
57 }
58 }
59 _lowestBatteryId = lowestBatteryId
60 return
61 }
62 }
63
64 // Couldn't determine lowest battery, show all
65 _lowestBatteryId = -1
66 }
67
68 function _recalcLowestBatteryIdFromPercentage() {
69 if (_activeVehicle) {
70 // If there is only one battery then it is the lowest
71 if (_activeVehicle.batteries.count === 1) {
72 _lowestBatteryId = _activeVehicle.batteries.get(0).id.rawValue
73 return
74 }
75
76 // If we have valid percentage for all batteries we use that to determine lowest battery
77 let allHavePercentage = true
78 for (var i = 0; i < _activeVehicle.batteries.count; i++) {
79 let battery = _activeVehicle.batteries.get(i)
80 if (isNaN(battery.percentRemaining.rawValue)) {
81 allHavePercentage = false
82 break
83 }
84 }
85 if (allHavePercentage) {
86 let lowestBattery = _activeVehicle.batteries.get(0)
87 let lowestBatteryId = lowestBattery.id.rawValue
88 for (var i = 1; i < _activeVehicle.batteries.count; i++) {
89 let battery = _activeVehicle.batteries.get(i)
90 if (battery.percentRemaining.rawValue < lowestBattery.percentRemaining.rawValue) {
91 lowestBattery = battery
92 lowestBatteryId = battery.id.rawValue
93 }
94 }
95 _lowestBatteryId = lowestBatteryId
96 return
97 }
98 }
99
100 // Couldn't determine lowest battery, show all
101 _lowestBatteryId = -1
102 }
103
104 function _recalcLowestBatteryIdFromChargeState() {
105 if (_activeVehicle) {
106 // If there is only one battery then it is the lowest
107 if (_activeVehicle.batteries.count === 1) {
108 _lowestBatteryId = _activeVehicle.batteries.get(0).id.rawValue
109 return
110 }
111
112 // If we have valid chargeState for all batteries we use that to determine lowest battery
113 let allHaveChargeState = true
114 for (var i = 0; i < _activeVehicle.batteries.count; i++) {
115 let battery = _activeVehicle.batteries.get(i)
116 if (battery.chargeState.rawValue === MAVLink.MAV_BATTERY_CHARGE_STATE_UNDEFINED) {
117 allHaveChargeState = false
118 break
119 }
120 }
121 if (allHaveChargeState) {
122 let lowestBattery = _activeVehicle.batteries.get(0)
123 let lowestBatteryId = lowestBattery.id.rawValue
124 for (var i = 1; i < _activeVehicle.batteries.count; i++) {
125 let battery = _activeVehicle.batteries.get(i)
126 if (battery.chargeState.rawValue > lowestBattery.chargeState.rawValue) {
127 lowestBattery = battery
128 lowestBatteryId = battery.id.rawValue
129 }
130 }
131 _lowestBatteryId = lowestBatteryId
132 return
133 }
134 }
135
136 // Couldn't determine lowest battery, show all
137 _lowestBatteryId = -1
138 }
139
140 function _recalcLowestBatteryId() {
141 if (!_activeVehicle || _activeVehicle.batteries.count === 0) {
142 _lowestBatteryId = -1
143 return
144 }
145 if (_batterySettings.valueDisplay.rawValue === 0) {
146 // User wants percentage display so use that if available
147 _recalcLowestBatteryIdFromPercentage()
148 } else if (_batterySettings.valueDisplay.rawValue === 1) {
149 // User wants voltage display so use that if available
150 _recalcLowestBatteryIdFromVoltage()
151 }
152 // If we still dont have a lowest battery id then try charge state
153 if (_lowestBatteryId === -1) {
154 _recalcLowestBatteryIdFromChargeState()
155 }
156 }
157
158 Component.onCompleted: _recalcLowestBatteryId()
159
160 Connections {
161 target: _activeVehicle ? _activeVehicle.batteries : null
162 function onCountChanged() {_recalcLowestBatteryId() }
163 }
164
165 QGCPalette { id: qgcPal }
166
167 RowLayout {
168 id: batteryIndicatorRow
169 anchors.top: parent.top
170 anchors.bottom: parent.bottom
171 spacing: ScreenTools.defaultFontPixelWidth / 2
172
173 Repeater {
174 model: _activeVehicle ? _activeVehicle.batteries : 0
175
176 Loader {
177 Layout.fillHeight: true
178 sourceComponent: batteryVisual
179 visible: control._lowestBatteryId === -1 || object.id.rawValue === control._lowestBatteryId || !control._batterySettings.consolidateMultipleBatteries.rawValue
180
181 property var battery: object
182 }
183 }
184 }
185
186 MouseArea {
187 anchors.fill: parent
188 onClicked: mainWindow.showIndicatorDrawer(batteryPopup, control)
189 }
190
191 Component {
192 id: batteryPopup
193
194 ToolIndicatorPage {
195 showExpand: expandedComponent ? true : false
196 waitForParameters: control.waitForParameters
197 contentComponent: batteryContentComponent
198 expandedComponent: batteryExpandedComponent
199 }
200 }
201
202 Component {
203 id: batteryVisual
204
205 Row {
206 Layout.fillHeight: true
207 spacing: ScreenTools.defaultFontPixelWidth / 4
208
209 function getBatteryColor() {
210 switch (battery.chargeState.rawValue) {
211 case MAVLink.MAV_BATTERY_CHARGE_STATE_OK:
212 if (!isNaN(battery.percentRemaining.rawValue)) {
213 if (battery.percentRemaining.rawValue > threshold1) {
214 return qgcPal.colorGreen
215 } else if (battery.percentRemaining.rawValue > threshold2) {
216 return qgcPal.colorYellowGreen
217 } else {
218 return qgcPal.colorYellow
219 }
220 } else {
221 return qgcPal.text
222 }
223 case MAVLink.MAV_BATTERY_CHARGE_STATE_LOW:
224 return qgcPal.colorOrange
225 case MAVLink.MAV_BATTERY_CHARGE_STATE_CRITICAL:
226 case MAVLink.MAV_BATTERY_CHARGE_STATE_EMERGENCY:
227 case MAVLink.MAV_BATTERY_CHARGE_STATE_FAILED:
228 case MAVLink.MAV_BATTERY_CHARGE_STATE_UNHEALTHY:
229 return qgcPal.colorRed
230 default:
231 return qgcPal.text
232 }
233 }
234
235 function getBatterySvgSource() {
236 switch (battery.chargeState.rawValue) {
237 case MAVLink.MAV_BATTERY_CHARGE_STATE_OK:
238 if (!isNaN(battery.percentRemaining.rawValue)) {
239 if (battery.percentRemaining.rawValue > threshold1) {
240 return "/qmlimages/BatteryGreen.svg"
241 } else if (battery.percentRemaining.rawValue > threshold2) {
242 return "/qmlimages/BatteryYellowGreen.svg"
243 } else {
244 return "/qmlimages/BatteryYellow.svg"
245 }
246 }
247 case MAVLink.MAV_BATTERY_CHARGE_STATE_LOW:
248 return "/qmlimages/BatteryOrange.svg" // Low with orange svg
249 case MAVLink.MAV_BATTERY_CHARGE_STATE_CRITICAL:
250 return "/qmlimages/BatteryCritical.svg" // Critical with red svg
251 case MAVLink.MAV_BATTERY_CHARGE_STATE_EMERGENCY:
252 case MAVLink.MAV_BATTERY_CHARGE_STATE_FAILED:
253 case MAVLink.MAV_BATTERY_CHARGE_STATE_UNHEALTHY:
254 return "/qmlimages/BatteryEMERGENCY.svg" // Exclamation mark
255 default:
256 return "/qmlimages/Battery.svg" // Fallback if percentage is unavailable
257 }
258 }
259
260 function getBatteryPercentageText() {
261 if (!isNaN(battery.percentRemaining.rawValue)) {
262 if (battery.percentRemaining.rawValue > 98.9) {
263 return qsTr("100%")
264 } else {
265 return battery.percentRemaining.valueString + battery.percentRemaining.units
266 }
267 } else if (!isNaN(battery.voltage.rawValue)) {
268 return battery.voltage.valueString + battery.voltage.units
269 } else if (battery.chargeState.rawValue !== MAVLink.MAV_BATTERY_CHARGE_STATE_UNDEFINED) {
270 return battery.chargeState.enumStringValue
271 }
272 return qsTr("n/a")
273 }
274
275 function getBatteryVoltageText() {
276 if (!isNaN(battery.voltage.rawValue)) {
277 return battery.voltage.valueString + battery.voltage.units
278 } else if (battery.chargeState.rawValue !== MAVLink.MAV_BATTERY_CHARGE_STATE_UNDEFINED) {
279 return battery.chargeState.enumStringValue
280 }
281 return qsTr("n/a")
282 }
283
284 Timer {
285 id: debounceRecalcTimer
286 interval: 50
287 running: false
288 repeat: false
289 onTriggered: {
290 control._recalcLowestBatteryId()
291 }
292 }
293 Connections {
294 target: battery.percentRemaining
295 function onRawValueChanged() {
296 debounceRecalcTimer.restart()
297 }
298 }
299 Connections {
300 target: battery.voltage
301 function onRawValueChanged() {
302 debounceRecalcTimer.restart()
303 }
304 }
305 Connections {
306 target: battery.chargeState
307 function onRawValueChanged() {
308 debounceRecalcTimer.restart()
309 }
310 }
311
312 QGCColoredImage {
313 anchors.top: parent.top
314 anchors.bottom: parent.bottom
315 width: height
316 sourceSize.width: width
317 source: getBatterySvgSource()
318 fillMode: Image.PreserveAspectFit
319 color: getBatteryColor()
320 }
321
322 ColumnLayout {
323 id: batteryInfoColumn
324 anchors.top: parent.top
325 anchors.bottom: parent.bottom
326 spacing: 0
327
328 QGCLabel {
329 Layout.alignment: Qt.AlignHCenter
330 verticalAlignment: Text.AlignVCenter
331 color: qgcPal.windowTransparentText
332 text: getBatteryPercentageText()
333 font.pointSize: _showBoth ? ScreenTools.defaultFontPointSize : ScreenTools.mediumFontPointSize
334 visible: _showBoth || _showPercentage
335 }
336
337 QGCLabel {
338 Layout.alignment: Qt.AlignHCenter
339 font.pointSize: _showBoth ? ScreenTools.defaultFontPointSize : ScreenTools.mediumFontPointSize
340 color: qgcPal.windowTransparentText
341 text: getBatteryVoltageText()
342 visible: _showBoth || _showVoltage
343 }
344 }
345 }
346 }
347
348 Component {
349 id: batteryContentComponent
350
351 ColumnLayout {
352 spacing: ScreenTools.defaultFontPixelHeight / 2
353
354 Component {
355 id: batteryValuesAvailableComponent
356
357 QtObject {
358 property bool functionAvailable: battery.function.rawValue !== MAVLink.MAV_BATTERY_FUNCTION_UNKNOWN
359 property bool showFunction: functionAvailable && battery.function.rawValue != MAVLink.MAV_BATTERY_FUNCTION_ALL
360 property bool temperatureAvailable: !isNaN(battery.temperature.rawValue)
361 property bool currentAvailable: !isNaN(battery.current.rawValue)
362 property bool mahConsumedAvailable: !isNaN(battery.mahConsumed.rawValue)
363 property bool timeRemainingAvailable: !isNaN(battery.timeRemaining.rawValue)
364 property bool percentRemainingAvailable: !isNaN(battery.percentRemaining.rawValue)
365 property bool chargeStateAvailable: battery.chargeState.rawValue !== MAVLink.MAV_BATTERY_CHARGE_STATE_UNDEFINED
366 }
367 }
368
369 Repeater {
370 model: _activeVehicle ? _activeVehicle.batteries : 0
371
372 SettingsGroupLayout {
373 heading: qsTr("Battery %1").arg(_activeVehicle.batteries.length === 1 ? qsTr("Status") : object.id.rawValue)
374 contentSpacing: 0
375 showDividers: false
376
377 property var batteryValuesAvailable: batteryValuesAvailableLoader.item
378
379 Loader {
380 id: batteryValuesAvailableLoader
381 sourceComponent: batteryValuesAvailableComponent
382
383 property var battery: object
384 }
385
386 LabelledLabel {
387 label: qsTr("Charge State")
388 labelText: object.chargeState.enumStringValue
389 visible: batteryValuesAvailable.chargeStateAvailable
390 }
391
392 LabelledLabel {
393 label: qsTr("Remaining")
394 labelText: object.timeRemainingStr.value
395 visible: batteryValuesAvailable.timeRemainingAvailable
396 }
397
398 LabelledLabel {
399 label: qsTr("Remaining")
400 labelText: object.percentRemaining.valueString + " " + object.percentRemaining.units
401 visible: batteryValuesAvailable.percentRemainingAvailable
402 }
403
404 LabelledLabel {
405 label: qsTr("Voltage")
406 labelText: object.voltage.valueString + " " + object.voltage.units
407 }
408
409 LabelledLabel {
410 label: qsTr("Consumed")
411 labelText: object.mahConsumed.valueString + " " + object.mahConsumed.units
412 visible: batteryValuesAvailable.mahConsumedAvailable
413 }
414
415 LabelledLabel {
416 label: qsTr("Temperature")
417 labelText: object.temperature.valueString + " " + object.temperature.units
418 visible: batteryValuesAvailable.temperatureAvailable
419 }
420
421 LabelledLabel {
422 label: qsTr("Function")
423 labelText: object.function.enumStringValue
424 visible: batteryValuesAvailable.showFunction
425 }
426 }
427 }
428 }
429 }
430
431 Component {
432 id: batteryExpandedComponent
433
434 ColumnLayout {
435 spacing: ScreenTools.defaultFontPixelHeight / 2
436
437 property real batteryIconHeight: ScreenTools.defaultFontPixelWidth * 3
438
439 FactPanelController { id: controller }
440
441 SettingsGroupLayout {
442 heading: qsTr("Battery Display")
443 Layout.fillWidth: true
444
445 FactCheckBoxSlider {
446 Layout.fillWidth: true
447 fact: _batterySettings.consolidateMultipleBatteries
448 text: qsTr("Only show battery with lowest charge")
449 visible: fact.visible
450 }
451
452 LabelledFactComboBox {
453 label: qsTr("Value")
454 fact: _batterySettings.valueDisplay
455 visible: fact.visible
456 }
457
458 ColumnLayout {
459 QGCLabel { text: qsTr("Coloring") }
460
461 RowLayout {
462 spacing: ScreenTools.defaultFontPixelWidth
463
464 // Battery 100%
465 RowLayout {
466 spacing: ScreenTools.defaultFontPixelWidth * 0.05 // Tighter spacing for icon and label
467 QGCColoredImage {
468 source: "/qmlimages/BatteryGreen.svg"
469 width: height
470 height: batteryIconHeight
471 fillMode: Image.PreserveAspectFit
472 color: qgcPal.colorGreen
473 }
474 QGCLabel { text: qsTr("100%") }
475 }
476
477 // Threshold 1
478 RowLayout {
479 spacing: ScreenTools.defaultFontPixelWidth * 0.05 // Tighter spacing for icon and field
480 QGCColoredImage {
481 source: "/qmlimages/BatteryYellowGreen.svg"
482 width: height
483 height: batteryIconHeight
484 fillMode: Image.PreserveAspectFit
485 color: qgcPal.colorYellowGreen
486 }
487 FactTextField {
488 id: threshold1Field
489 fact: _batterySettings.threshold1
490 implicitWidth: ScreenTools.defaultFontPixelWidth * 6
491 height: ScreenTools.defaultFontPixelHeight * 1.5
492 enabled: fact.visible
493 onEditingFinished: {
494 // Validate and set the new threshold value
495 _batterySettings.setThreshold1(parseInt(text));
496 }
497 }
498 }
499
500 // Threshold 2
501 RowLayout {
502 spacing: ScreenTools.defaultFontPixelWidth * 0.05 // Tighter spacing for icon and field
503 QGCColoredImage {
504 source: "/qmlimages/BatteryYellow.svg"
505 width: height
506 height: batteryIconHeight
507 fillMode: Image.PreserveAspectFit
508 color: qgcPal.colorYellow
509 }
510 FactTextField {
511 fact: _batterySettings.threshold2
512 implicitWidth: ScreenTools.defaultFontPixelWidth * 6
513 height: ScreenTools.defaultFontPixelHeight * 1.5
514 enabled: fact.visible
515 onEditingFinished: {
516 // Validate and set the new threshold value
517 _batterySettings.setThreshold2(parseInt(text));
518 }
519 }
520 }
521
522 // Low state
523 RowLayout {
524 spacing: ScreenTools.defaultFontPixelWidth * 0.05 // Tighter spacing for icon and label
525 QGCColoredImage {
526 source: "/qmlimages/BatteryOrange.svg"
527 width: height
528 height: batteryIconHeight
529 fillMode: Image.PreserveAspectFit
530 color: qgcPal.colorOrange
531 }
532 QGCLabel { text: qsTr("Low") }
533 }
534
535 // Critical state
536 RowLayout {
537 spacing: ScreenTools.defaultFontPixelWidth * 0.05 // Tighter spacing for icon and label
538 QGCColoredImage {
539 source: "/qmlimages/BatteryCritical.svg"
540 width: height
541 height: batteryIconHeight
542 fillMode: Image.PreserveAspectFit
543 color: qgcPal.colorRed
544 }
545 QGCLabel { text: qsTr("Critical") }
546 }
547 }
548 }
549 }
550
551 Loader {
552 Layout.fillWidth: true
553 source: _activeVehicle.expandedToolbarIndicatorSource("Battery")
554 }
555
556 SettingsGroupLayout {
557 visible: _activeVehicle.autopilotPlugin.knownVehicleComponentAvailable(AutoPilotPlugin.KnownPowerVehicleComponent) &&
558 QGroundControl.corePlugin.showAdvancedUI
559
560 LabelledButton {
561 label: qsTr("Vehicle Power")
562 buttonText: qsTr("Configure")
563
564 onClicked: {
565 mainWindow.showKnownVehicleComponentConfigPage(AutoPilotPlugin.KnownPowerVehicleComponent)
566 mainWindow.closeIndicatorDrawer()
567 }
568 }
569 }
570 }
571 }
572}