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: false
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 === MAVLinkEnums.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: false
197 expandedComponentWaitForParameters: true
198 contentComponent: batteryContentComponent
199 expandedComponent: batteryExpandedComponent
200 }
201 }
202
203 Component {
204 id: batteryVisual
205
206 Row {
207 Layout.fillHeight: true
208 spacing: ScreenTools.defaultFontPixelWidth / 4
209
210 function getBatteryColor() {
211 switch (battery.chargeState.rawValue) {
212 case MAVLinkEnums.MAV_BATTERY_CHARGE_STATE_OK:
213 if (!isNaN(battery.percentRemaining.rawValue)) {
214 if (battery.percentRemaining.rawValue > threshold1) {
215 return qgcPal.colorGreen
216 } else if (battery.percentRemaining.rawValue > threshold2) {
217 return qgcPal.colorYellowGreen
218 } else {
219 return qgcPal.colorYellow
220 }
221 } else {
222 return qgcPal.text
223 }
224 case MAVLinkEnums.MAV_BATTERY_CHARGE_STATE_LOW:
225 return qgcPal.colorOrange
226 case MAVLinkEnums.MAV_BATTERY_CHARGE_STATE_CRITICAL:
227 case MAVLinkEnums.MAV_BATTERY_CHARGE_STATE_EMERGENCY:
228 case MAVLinkEnums.MAV_BATTERY_CHARGE_STATE_FAILED:
229 case MAVLinkEnums.MAV_BATTERY_CHARGE_STATE_UNHEALTHY:
230 return qgcPal.colorRed
231 default:
232 return qgcPal.text
233 }
234 }
235
236 function getBatterySvgSource() {
237 switch (battery.chargeState.rawValue) {
238 case MAVLinkEnums.MAV_BATTERY_CHARGE_STATE_OK:
239 if (!isNaN(battery.percentRemaining.rawValue)) {
240 if (battery.percentRemaining.rawValue > threshold1) {
241 return "/qmlimages/BatteryGreen.svg"
242 } else if (battery.percentRemaining.rawValue > threshold2) {
243 return "/qmlimages/BatteryYellowGreen.svg"
244 } else {
245 return "/qmlimages/BatteryYellow.svg"
246 }
247 }
248 case MAVLinkEnums.MAV_BATTERY_CHARGE_STATE_LOW:
249 return "/qmlimages/BatteryOrange.svg" // Low with orange svg
250 case MAVLinkEnums.MAV_BATTERY_CHARGE_STATE_CRITICAL:
251 return "/qmlimages/BatteryCritical.svg" // Critical with red svg
252 case MAVLinkEnums.MAV_BATTERY_CHARGE_STATE_EMERGENCY:
253 case MAVLinkEnums.MAV_BATTERY_CHARGE_STATE_FAILED:
254 case MAVLinkEnums.MAV_BATTERY_CHARGE_STATE_UNHEALTHY:
255 return "/qmlimages/BatteryEMERGENCY.svg" // Exclamation mark
256 default:
257 return "/qmlimages/Battery.svg" // Fallback if percentage is unavailable
258 }
259 }
260
261 function getBatteryPercentageText() {
262 if (!isNaN(battery.percentRemaining.rawValue)) {
263 if (battery.percentRemaining.rawValue > 98.9) {
264 return qsTr("100%")
265 } else {
266 return battery.percentRemaining.valueString + battery.percentRemaining.units
267 }
268 } else if (!isNaN(battery.voltage.rawValue)) {
269 return battery.voltage.valueString + battery.voltage.units
270 } else if (battery.chargeState.rawValue !== MAVLinkEnums.MAV_BATTERY_CHARGE_STATE_UNDEFINED) {
271 return battery.chargeState.enumStringValue
272 }
273 return qsTr("n/a")
274 }
275
276 function getBatteryVoltageText() {
277 if (!isNaN(battery.voltage.rawValue)) {
278 return battery.voltage.valueString + battery.voltage.units
279 } else if (battery.chargeState.rawValue !== MAVLinkEnums.MAV_BATTERY_CHARGE_STATE_UNDEFINED) {
280 return battery.chargeState.enumStringValue
281 }
282 return qsTr("n/a")
283 }
284
285 Timer {
286 id: debounceRecalcTimer
287 interval: 50
288 running: false
289 repeat: false
290 onTriggered: {
291 control._recalcLowestBatteryId()
292 }
293 }
294 Connections {
295 target: battery.percentRemaining
296 function onRawValueChanged() {
297 debounceRecalcTimer.restart()
298 }
299 }
300 Connections {
301 target: battery.voltage
302 function onRawValueChanged() {
303 debounceRecalcTimer.restart()
304 }
305 }
306 Connections {
307 target: battery.chargeState
308 function onRawValueChanged() {
309 debounceRecalcTimer.restart()
310 }
311 }
312
313 QGCColoredImage {
314 anchors.top: parent.top
315 anchors.bottom: parent.bottom
316 width: height
317 sourceSize.width: width
318 source: getBatterySvgSource()
319 fillMode: Image.PreserveAspectFit
320 color: getBatteryColor()
321 }
322
323 ColumnLayout {
324 id: batteryInfoColumn
325 anchors.top: parent.top
326 anchors.bottom: parent.bottom
327 spacing: 0
328
329 QGCLabel {
330 Layout.alignment: Qt.AlignHCenter
331 verticalAlignment: Text.AlignVCenter
332 color: qgcPal.text
333 text: getBatteryPercentageText()
334 font.pointSize: _showBoth ? ScreenTools.defaultFontPointSize : ScreenTools.mediumFontPointSize
335 visible: _showBoth || _showPercentage
336 }
337
338 QGCLabel {
339 Layout.alignment: Qt.AlignHCenter
340 font.pointSize: _showBoth ? ScreenTools.defaultFontPointSize : ScreenTools.mediumFontPointSize
341 color: qgcPal.text
342 text: getBatteryVoltageText()
343 visible: _showBoth || _showVoltage
344 }
345 }
346 }
347 }
348
349 Component {
350 id: batteryContentComponent
351
352 ColumnLayout {
353 spacing: ScreenTools.defaultFontPixelHeight / 2
354
355 Component {
356 id: batteryValuesAvailableComponent
357
358 QtObject {
359 property bool functionAvailable: battery.function.rawValue !== MAVLinkEnums.MAV_BATTERY_FUNCTION_UNKNOWN
360 property bool showFunction: functionAvailable && battery.function.rawValue != MAVLinkEnums.MAV_BATTERY_FUNCTION_ALL
361 property bool temperatureAvailable: !isNaN(battery.temperature.rawValue)
362 property bool currentAvailable: !isNaN(battery.current.rawValue)
363 property bool mahConsumedAvailable: !isNaN(battery.mahConsumed.rawValue)
364 property bool timeRemainingAvailable: !isNaN(battery.timeRemaining.rawValue)
365 property bool percentRemainingAvailable: !isNaN(battery.percentRemaining.rawValue)
366 property bool chargeStateAvailable: battery.chargeState.rawValue !== MAVLinkEnums.MAV_BATTERY_CHARGE_STATE_UNDEFINED
367 }
368 }
369
370 Repeater {
371 model: _activeVehicle ? _activeVehicle.batteries : 0
372
373 SettingsGroupLayout {
374 heading: qsTr("Battery %1").arg(_activeVehicle.batteries.length === 1 ? qsTr("Status") : object.id.rawValue)
375 contentSpacing: 0
376 showDividers: false
377
378 property var batteryValuesAvailable: batteryValuesAvailableLoader.item
379
380 Loader {
381 id: batteryValuesAvailableLoader
382 sourceComponent: batteryValuesAvailableComponent
383
384 property var battery: object
385 }
386
387 LabelledLabel {
388 label: qsTr("Charge State")
389 labelText: object.chargeState.enumStringValue
390 visible: batteryValuesAvailable.chargeStateAvailable
391 }
392
393 LabelledLabel {
394 label: qsTr("Remaining")
395 labelText: object.timeRemainingStr.value
396 visible: batteryValuesAvailable.timeRemainingAvailable
397 }
398
399 LabelledLabel {
400 label: qsTr("Remaining")
401 labelText: object.percentRemaining.valueString + " " + object.percentRemaining.units
402 visible: batteryValuesAvailable.percentRemainingAvailable
403 }
404
405 LabelledLabel {
406 label: qsTr("Voltage")
407 labelText: object.voltage.valueString + " " + object.voltage.units
408 }
409
410 LabelledLabel {
411 label: qsTr("Consumed")
412 labelText: object.mahConsumed.valueString + " " + object.mahConsumed.units
413 visible: batteryValuesAvailable.mahConsumedAvailable
414 }
415
416 LabelledLabel {
417 label: qsTr("Temperature")
418 labelText: object.temperature.valueString + " " + object.temperature.units
419 visible: batteryValuesAvailable.temperatureAvailable
420 }
421
422 LabelledLabel {
423 label: qsTr("Function")
424 labelText: object.function.enumStringValue
425 visible: batteryValuesAvailable.showFunction
426 }
427 }
428 }
429 }
430 }
431
432 Component {
433 id: batteryExpandedComponent
434
435 ColumnLayout {
436 spacing: ScreenTools.defaultFontPixelHeight / 2
437
438 property real batteryIconHeight: ScreenTools.defaultFontPixelWidth * 3
439
440 FactPanelController { id: controller }
441
442 SettingsGroupLayout {
443 heading: qsTr("Battery Display")
444 Layout.fillWidth: true
445
446 FactCheckBoxSlider {
447 Layout.fillWidth: true
448 fact: _batterySettings.consolidateMultipleBatteries
449 text: qsTr("Only show battery with lowest charge")
450 visible: fact.userVisible
451 }
452
453 LabelledFactComboBox {
454 label: qsTr("Value")
455 fact: _batterySettings.valueDisplay
456 visible: fact.userVisible
457 }
458
459 ColumnLayout {
460 QGCLabel { text: qsTr("Coloring") }
461
462 RowLayout {
463 spacing: ScreenTools.defaultFontPixelWidth
464
465 // Battery 100%
466 RowLayout {
467 spacing: ScreenTools.defaultFontPixelWidth * 0.05 // Tighter spacing for icon and label
468 QGCColoredImage {
469 source: "/qmlimages/BatteryGreen.svg"
470 width: height
471 height: batteryIconHeight
472 fillMode: Image.PreserveAspectFit
473 color: qgcPal.colorGreen
474 }
475 QGCLabel { text: qsTr("100%") }
476 }
477
478 // Threshold 1
479 RowLayout {
480 spacing: ScreenTools.defaultFontPixelWidth * 0.05 // Tighter spacing for icon and field
481 QGCColoredImage {
482 source: "/qmlimages/BatteryYellowGreen.svg"
483 width: height
484 height: batteryIconHeight
485 fillMode: Image.PreserveAspectFit
486 color: qgcPal.colorYellowGreen
487 }
488 FactTextField {
489 id: threshold1Field
490 fact: _batterySettings.threshold1
491 implicitWidth: ScreenTools.defaultFontPixelWidth * 6
492 height: ScreenTools.defaultFontPixelHeight * 1.5
493 enabled: fact.userVisible
494 onEditingFinished: {
495 // Validate and set the new threshold value
496 _batterySettings.setThreshold1(parseInt(text));
497 }
498 }
499 }
500
501 // Threshold 2
502 RowLayout {
503 spacing: ScreenTools.defaultFontPixelWidth * 0.05 // Tighter spacing for icon and field
504 QGCColoredImage {
505 source: "/qmlimages/BatteryYellow.svg"
506 width: height
507 height: batteryIconHeight
508 fillMode: Image.PreserveAspectFit
509 color: qgcPal.colorYellow
510 }
511 FactTextField {
512 fact: _batterySettings.threshold2
513 implicitWidth: ScreenTools.defaultFontPixelWidth * 6
514 height: ScreenTools.defaultFontPixelHeight * 1.5
515 enabled: fact.userVisible
516 onEditingFinished: {
517 // Validate and set the new threshold value
518 _batterySettings.setThreshold2(parseInt(text));
519 }
520 }
521 }
522
523 // Low state
524 RowLayout {
525 spacing: ScreenTools.defaultFontPixelWidth * 0.05 // Tighter spacing for icon and label
526 QGCColoredImage {
527 source: "/qmlimages/BatteryOrange.svg"
528 width: height
529 height: batteryIconHeight
530 fillMode: Image.PreserveAspectFit
531 color: qgcPal.colorOrange
532 }
533 QGCLabel { text: qsTr("Low") }
534 }
535
536 // Critical state
537 RowLayout {
538 spacing: ScreenTools.defaultFontPixelWidth * 0.05 // Tighter spacing for icon and label
539 QGCColoredImage {
540 source: "/qmlimages/BatteryCritical.svg"
541 width: height
542 height: batteryIconHeight
543 fillMode: Image.PreserveAspectFit
544 color: qgcPal.colorRed
545 }
546 QGCLabel { text: qsTr("Critical") }
547 }
548 }
549 }
550 }
551
552 Loader {
553 Layout.fillWidth: true
554 source: _activeVehicle.expandedToolbarIndicatorSource("Battery")
555 }
556
557 SettingsGroupLayout {
558 visible: _activeVehicle.autopilotPlugin.knownVehicleComponentAvailable(AutoPilotPlugin.KnownPowerVehicleComponent) &&
559 QGroundControl.corePlugin.showAdvancedUI
560
561 LabelledButton {
562 label: qsTr("Vehicle Power")
563 buttonText: qsTr("Configure")
564
565 onClicked: {
566 mainWindow.showKnownVehicleComponentConfigPage(AutoPilotPlugin.KnownPowerVehicleComponent)
567 mainWindow.closeIndicatorDrawer()
568 }
569 }
570 }
571 }
572 }
573}