QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
BluetoothSettings.qml
Go to the documentation of this file.
1pragma ComponentBehavior: Bound
2
3import QtQuick
4import QtQuick.Controls
5import QtQuick.Layouts
6
7import QGroundControl
8import QGroundControl.Controls
9
10ColumnLayout {
11 id: root
12
13 readonly property var screenTools: ScreenTools
14 readonly property var qgc: QGroundControl
15 readonly property var btConfig: BluetoothConfiguration
16 QGCPalette { id: qgcPal; colorGroupEnabled: root.enabled }
17 readonly property var palette: qgcPal
18
19 // Pull shared settings values from LinkSettings Loader when available.
20 property var subEditConfig: (root.parent && root.parent.subEditConfig !== undefined) ? root.parent.subEditConfig : null
21 readonly property real _secondColumnWidth: (root.parent && root.parent._secondColumnWidth !== undefined) ? root.parent._secondColumnWidth : (root.screenTools.defaultFontPixelWidth * 30)
22 readonly property real _rowSpacing: (root.parent && root.parent._rowSpacing !== undefined) ? root.parent._rowSpacing : (root.screenTools.defaultFontPixelHeight / 2)
23 readonly property real _colSpacing: (root.parent && root.parent._colSpacing !== undefined) ? root.parent._colSpacing : (root.screenTools.defaultFontPixelWidth / 2)
24
25 spacing: root._rowSpacing
26
27 visible: root.subEditConfig !== null
28
29 function saveSettings() { }
30
31 //-- Properties --
32 property bool paired: false
33
34 readonly property bool isBleMode: root.subEditConfig ? root.subEditConfig.mode === root.btConfig.BluetoothMode.ModeLowEnergy : false
35 readonly property bool isClassicMode: root.subEditConfig ? root.subEditConfig.mode === root.btConfig.BluetoothMode.ModeClassic : false
36 readonly property string currentAddress: root.subEditConfig ? root.subEditConfig.address : ""
37 readonly property bool hasAdapter: root.subEditConfig ? root.subEditConfig.adapterAvailable : false
38 readonly property bool adapterOn: root.subEditConfig ? root.subEditConfig.adapterPoweredOn : false
39 readonly property bool isScanning: root.subEditConfig ? root.subEditConfig.scanning : false
40
41 property var knownDevices: []
42 property var availableAdapters: []
43 property bool _initialAdapterSelected: false
44
45 function toArray(listLike) {
46 if (!listLike) {
47 return []
48 }
49 if (Array.isArray(listLike)) {
50 return listLike.slice()
51 }
52
53 var arr = []
54 var length = 0
55 if (typeof listLike.length === "number") {
56 length = listLike.length
57 for (var i = 0; i < length; i++) {
58 arr.push(listLike[i])
59 }
60 return arr
61 }
62 if (typeof listLike.count === "number" && typeof listLike.get === "function") {
63 length = listLike.count
64 for (var j = 0; j < length; j++) {
65 arr.push(listLike.get(j))
66 }
67 return arr
68 }
69 return arr
70 }
71
72 // Sorted device list
73 readonly property var sortedDevices: {
74 if (!root.subEditConfig) return []
75 var arr = root.toArray(root.subEditConfig.devicesModel)
76
77 if (root.isBleMode) {
78 arr.sort(function(a, b) {
79 var rssiA = (typeof a === "object" && a.rssi) ? a.rssi : -999
80 var rssiB = (typeof b === "object" && b.rssi) ? b.rssi : -999
81 return rssiB - rssiA
82 })
83 } else {
84 arr.sort(function(a, b) {
85 var nameA = (typeof a === "object" ? a.name : a) || ""
86 var nameB = (typeof b === "object" ? b.name : b) || ""
87 return nameA.localeCompare(nameB)
88 })
89 }
90 return arr
91 }
92
93 //-- Helper Functions --
94 function refreshDeviceLists() {
95 if (!root.subEditConfig) {
96 root.knownDevices = []
97 root.availableAdapters = []
98 _initialAdapterSelected = false
99 return
100 }
101 var connected = root.subEditConfig.getConnectedDevices() || []
102 var pairedDevices = root.subEditConfig.getAllPairedDevices() || []
103 var seen = {}
104 var merged = []
105
106 for (var i = 0; i < connected.length; i++) {
107 var dev = connected[i]
108 if (dev.address && !seen[dev.address]) {
109 seen[dev.address] = true
110 merged.push({ name: dev.name, address: dev.address, isConnected: true })
111 }
112 }
113 for (var j = 0; j < pairedDevices.length; j++) {
114 var pdev = pairedDevices[j]
115 if (pdev.address && !seen[pdev.address]) {
116 seen[pdev.address] = true
117 merged.push({ name: pdev.name, address: pdev.address, isConnected: false })
118 }
119 }
120 root.knownDevices = merged
121 root.availableAdapters = root.subEditConfig.getAllAvailableAdapters() || []
122
123 if (root.subEditConfig.adapterAddress && root.subEditConfig.adapterAddress !== "") {
124 _initialAdapterSelected = true
125 }
126
127 if (!_initialAdapterSelected && root.availableAdapters.length > 0 && (!root.subEditConfig.adapterAddress || root.subEditConfig.adapterAddress === "")) {
128 const firstAddress = root.availableAdapters[0].address || ""
129 if (firstAddress !== "") {
130 root.subEditConfig.selectAdapter(firstAddress)
131 _initialAdapterSelected = (root.subEditConfig.adapterAddress === firstAddress)
132 }
133 }
134 }
135
136 function updatePairingStatus() {
137 if (!root.subEditConfig) {
138 root.paired = false
139 return
140 }
141 root.paired = root.isClassicMode && root.currentAddress !== "" &&
142 root.subEditConfig.isPaired(root.currentAddress)
143 }
144
145 function rssiToSignalLevel(rssi) {
146 if (rssi >= -50) return 4
147 if (rssi >= -60) return 3
148 if (rssi >= -70) return 2
149 if (rssi >= -80) return 1
150 return 0
151 }
152
153 //-- Signal Connections --
154 Connections {
155 target: root.subEditConfig
156 enabled: root.subEditConfig !== null
157
158 function onDevicesModelChanged() { root.refreshDeviceLists(); root.updatePairingStatus() }
159 function onDeviceChanged() { root.updatePairingStatus() }
160 function onModeChanged() { root.refreshDeviceLists(); root.updatePairingStatus() }
161 function onAdapterStateChanged() { root.refreshDeviceLists() }
162 function onPairingStatusChanged(){ root.refreshDeviceLists(); root.updatePairingStatus() }
163 function onErrorOccurred(error) { root.qgc.showMessageDialog(root, qsTr("Bluetooth Error"), error, Dialog.Ok) }
164 }
165
166 Component.onCompleted: {
167 refreshDeviceLists()
168 updatePairingStatus()
169 }
170
171 onSubEditConfigChanged: {
172 _initialAdapterSelected = false
173 refreshDeviceLists()
174 updatePairingStatus()
175 }
176
177 //==========================================================================
178 //-- Bluetooth Adapter Section --
179 //==========================================================================
180 SectionHeader {
181 Layout.fillWidth: true
182 text: qsTr("Bluetooth Adapter")
183 }
184
185 GridLayout {
186 columns: 2
187 columnSpacing: root._colSpacing
188 rowSpacing: root._rowSpacing
189 Layout.fillWidth: true
190
191 QGCLabel {
192 text: qsTr("Adapter")
193 visible: root.hasAdapter
194 }
195 QGCComboBox {
196 id: adapterCombo
197 Layout.preferredWidth: root._secondColumnWidth
198 visible: root.hasAdapter
199 sizeToContents: false
200 clip: true
201
202 // Column is ~30 chars, MAC + parens = 20 chars, leaves ~10 for name
203 readonly property int maxNameLength: 10
204
205 model: {
206 var result = []
207 for (var i = 0; i < root.availableAdapters.length; i++) {
208 var a = root.availableAdapters[i]
209 var name = a.name || qsTr("Unknown")
210 if (name.length > maxNameLength) {
211 name = name.substring(0, maxNameLength - 1) + "…"
212 }
213 result.push(name + " (" + a.address + ")")
214 }
215 return result
216 }
217 currentIndex: {
218 var currentAddr = root.subEditConfig ? root.subEditConfig.adapterAddress : ""
219 for (var i = 0; i < root.availableAdapters.length; i++) {
220 if (root.availableAdapters[i].address === currentAddr) return i
221 }
222 return 0
223 }
224 onActivated: function(index) {
225 if (root.subEditConfig && index >= 0 && index < root.availableAdapters.length) {
226 root.subEditConfig.selectAdapter(root.availableAdapters[index].address)
227 }
228 }
229 }
230
231 QGCLabel {
232 text: qsTr("Status")
233 visible: root.hasAdapter
234 }
235 QGCLabel {
236 Layout.preferredWidth: root._secondColumnWidth
237 text: root.subEditConfig ? root.subEditConfig.hostMode : ""
238 visible: root.hasAdapter
239 color: root.adapterOn ? root.palette.text : root.palette.warningText
240 }
241
242 QGCLabel {
243 text: qsTr("Bluetooth adapter unavailable")
244 visible: !root.hasAdapter
245 color: root.palette.warningText
246 Layout.columnSpan: 2
247 }
248 }
249
250 RowLayout {
251 Layout.fillWidth: true
252 spacing: root._colSpacing
253 visible: root.hasAdapter
254
255 QGCCheckBox {
256 text: qsTr("Powered On")
257 checked: root.adapterOn
258 onClicked: {
259 if (!root.subEditConfig) return
260 root.adapterOn ? root.subEditConfig.powerOffAdapter() : root.subEditConfig.powerOnAdapter()
261 }
262 }
263
264 QGCCheckBox {
265 id: discoverableCheck
266 text: qsTr("Discoverable")
267 checked: root.subEditConfig ? root.subEditConfig.adapterDiscoverable : false
268 enabled: root.adapterOn
269 onClicked: { if (root.subEditConfig) root.subEditConfig.setAdapterDiscoverable(discoverableCheck.checked) }
270 }
271 }
272
273 //==========================================================================
274 //-- Connection Settings Section --
275 //==========================================================================
276 SectionHeader {
277 Layout.fillWidth: true
278 text: qsTr("Connection")
279 }
280
281 GridLayout {
282 columns: 2
283 columnSpacing: root._colSpacing
284 rowSpacing: root._rowSpacing
285 Layout.fillWidth: true
286
287 QGCLabel { text: qsTr("Mode") }
288 RowLayout {
289 Layout.preferredWidth: root._secondColumnWidth
290 spacing: root._colSpacing
291
292 QGCRadioButton {
293 text: qsTr("Classic")
294 checked: root.isClassicMode
295 onClicked: { if (root.subEditConfig) root.subEditConfig.mode = root.btConfig.BluetoothMode.ModeClassic }
296 }
297
298 QGCRadioButton {
299 text: qsTr("BLE")
300 checked: root.isBleMode
301 onClicked: { if (root.subEditConfig) root.subEditConfig.mode = root.btConfig.BluetoothMode.ModeLowEnergy }
302 }
303 }
304
305 QGCLabel { text: qsTr("Selected Device") }
306 QGCLabel {
307 Layout.preferredWidth: root._secondColumnWidth
308 text: root.subEditConfig && root.subEditConfig.deviceName ? root.subEditConfig.deviceName : qsTr("None")
309 }
310
311 QGCLabel { text: qsTr("Device Address") }
312 QGCLabel {
313 Layout.preferredWidth: root._secondColumnWidth
314 text: root.currentAddress || qsTr("N/A")
315 }
316
317 // Classic Bluetooth Pairing
318 QGCLabel {
319 text: qsTr("Pairing")
320 visible: root.isClassicMode && root.currentAddress !== ""
321 }
322 RowLayout {
323 Layout.preferredWidth: root._secondColumnWidth
324 visible: root.isClassicMode && root.currentAddress !== ""
325 spacing: root._colSpacing
326
327 QGCLabel {
328 text: root.subEditConfig ? root.subEditConfig.getPairingStatus(root.currentAddress) : ""
329 Layout.fillWidth: true
330 }
331
332 QGCButton {
333 text: root.paired ? qsTr("Unpair") : qsTr("Pair")
334 onClicked: {
335 if (!root.subEditConfig) return
336 root.paired ? root.subEditConfig.removePairing(root.currentAddress)
337 : root.subEditConfig.requestPairing(root.currentAddress)
338 }
339 }
340 }
341
342 // BLE Signal Strength
343 QGCLabel {
344 text: qsTr("Signal Strength")
345 visible: root.isBleMode && (rssiDisplay.hasConnected || rssiDisplay.hasSelected)
346 }
347 RowLayout {
348 id: rssiDisplay
349 Layout.preferredWidth: root._secondColumnWidth
350 visible: root.isBleMode && (hasConnected || hasSelected)
351 spacing: root.screenTools.defaultFontPixelWidth
352
353 readonly property bool hasConnected: root.subEditConfig && root.subEditConfig.connectedRssi !== 0
354 readonly property bool hasSelected: root.subEditConfig && root.subEditConfig.selectedRssi !== 0
355 readonly property int rssi: hasConnected ? root.subEditConfig.connectedRssi : (root.subEditConfig ? root.subEditConfig.selectedRssi : 0)
356 readonly property int signalLevel: root.rssiToSignalLevel(rssi)
357
358 Row {
359 id: rssiBars
360 spacing: 1
361 readonly property real barWidth: root.screenTools.defaultFontPixelWidth * 0.5
362 readonly property real maxBarHeight: root.screenTools.defaultFontPixelHeight
363
364 Repeater {
365 model: 4
366 delegate: Item {
367 id: rssiBarDelegate
368 required property int index
369 width: rssiBars.barWidth
370 height: rssiBars.maxBarHeight
371
372 Rectangle {
373 width: parent.width
374 height: root.screenTools.defaultFontPixelHeight * (0.4 + rssiBarDelegate.index * 0.2)
375 color: rssiBarDelegate.index < rssiDisplay.signalLevel ? root.palette.text : root.palette.buttonText
376 opacity: rssiBarDelegate.index < rssiDisplay.signalLevel ? 1.0 : 0.3
377 anchors.bottom: parent.bottom
378 }
379 }
380 }
381 }
382
383 QGCLabel {
384 text: rssiDisplay.rssi + " dBm"
385 color: rssiDisplay.hasConnected ? root.palette.text : root.palette.buttonText
386 }
387
388 QGCLabel {
389 text: rssiDisplay.hasConnected ? qsTr("(Connected)") : qsTr("(Last Scan)")
390 font.pointSize: root.screenTools.smallFontPointSize
391 color: root.palette.buttonText
392 }
393 }
394 }
395
396 //==========================================================================
397 //-- BLE Configuration Section (Collapsible) --
398 //==========================================================================
399 QGCCheckBox {
400 id: showBleConfig
401 text: qsTr("Advanced BLE Configuration")
402 visible: root.isBleMode
403 checked: false
404 }
405
406 GridLayout {
407 columns: 2
408 columnSpacing: root._colSpacing
409 rowSpacing: root._rowSpacing
410 Layout.fillWidth: true
411 visible: root.isBleMode && showBleConfig.checked
412
413 QGCLabel { text: qsTr("Service UUID") }
414 QGCTextField {
415 id: serviceUuidField
416 Layout.preferredWidth: root._secondColumnWidth
417 text: root.subEditConfig ? root.subEditConfig.serviceUuid : ""
418 placeholderText: qsTr("Auto-detect")
419 onEditingFinished: { if (root.subEditConfig) root.subEditConfig.serviceUuid = serviceUuidField.text }
420 }
421
422 QGCLabel { text: qsTr("RX Characteristic") }
423 QGCTextField {
424 id: readUuidField
425 Layout.preferredWidth: root._secondColumnWidth
426 text: root.subEditConfig ? root.subEditConfig.readUuid : ""
427 placeholderText: qsTr("Auto-detect")
428 onEditingFinished: { if (root.subEditConfig) root.subEditConfig.readUuid = readUuidField.text }
429 }
430
431 QGCLabel { text: qsTr("TX Characteristic") }
432 QGCTextField {
433 id: writeUuidField
434 Layout.preferredWidth: root._secondColumnWidth
435 text: root.subEditConfig ? root.subEditConfig.writeUuid : ""
436 placeholderText: qsTr("Auto-detect")
437 onEditingFinished: { if (root.subEditConfig) root.subEditConfig.writeUuid = writeUuidField.text }
438 }
439 }
440
441 QGCLabel {
442 text: qsTr("UUIDs are auto-detected for most devices. Only configure if connection fails.")
443 visible: root.isBleMode && showBleConfig.checked
444 wrapMode: Text.WordWrap
445 Layout.fillWidth: true
446 font.pointSize: root.screenTools.smallFontPointSize
447 color: root.palette.buttonText
448 }
449
450 //==========================================================================
451 //-- Known Devices Section (Classic only) --
452 //==========================================================================
453 SectionHeader {
454 Layout.fillWidth: true
455 text: qsTr("Known Devices")
456 visible: root.isClassicMode && root.knownDevices.length > 0
457 }
458
459 Flow {
460 Layout.fillWidth: true
461 spacing: root.screenTools.defaultFontPixelWidth
462 visible: root.isClassicMode && root.knownDevices.length > 0
463
464 Repeater {
465 model: root.knownDevices
466 delegate: QGCButton {
467 required property var modelData
468 property var dev: modelData
469 property bool isConnected: dev.isConnected === true
470
471 text: (dev.name || dev.address || qsTr("Unknown")) + (isConnected ? " [Connected]" : "")
472 checkable: true
473 checked: dev.address === root.currentAddress
474 onClicked: { if (root.subEditConfig && dev.address) root.subEditConfig.setDeviceByAddress(dev.address) }
475 }
476 }
477 }
478
479 //==========================================================================
480 //-- Available Devices Section --
481 //==========================================================================
482 SectionHeader {
483 Layout.fillWidth: true
484 text: root.isBleMode ? qsTr("Available BLE Devices") : qsTr("Available Devices")
485 }
486
487 // Scanning status
488 RowLayout {
489 Layout.fillWidth: true
490 visible: root.isScanning
491 spacing: root.screenTools.defaultFontPixelWidth
492
493 BusyIndicator {
494 Layout.preferredWidth: root.screenTools.defaultFontPixelHeight * 2
495 Layout.preferredHeight: Layout.preferredWidth
496 running: true
497 }
498
499 QGCLabel {
500 text: qsTr("Scanning for devices...")
501 color: root.palette.text
502 }
503 }
504
505 // Device list
506 QGCFlickable {
507 id: deviceList
508 Layout.fillWidth: true
509 Layout.preferredHeight: Math.min(deviceList.contentHeight, root.screenTools.defaultFontPixelHeight * 16)
510 contentHeight: deviceColumn.height
511 contentWidth: root.width
512 clip: true
513 visible: !root.isScanning || root.sortedDevices.length > 0
514
515 ColumnLayout {
516 id: deviceColumn
517 width: parent.width
518 spacing: root.screenTools.defaultFontPixelHeight * 0.25
519
520 Repeater {
521 id: deviceRepeater
522 model: root.sortedDevices
523 delegate: QGCButton {
524 id: deviceBtn
525 required property var modelData
526 Layout.fillWidth: true
527
528 property var dev: (typeof modelData === "object") ? modelData : ({ name: modelData })
529 property string deviceName: dev.name || ""
530 property string deviceAddress: dev.address || ""
531 property bool hasRssi: root.isBleMode && (typeof dev.rssi === "number") && dev.rssi !== 0
532 property int rssiVal: hasRssi ? dev.rssi : 0
533 property int signalLevel: hasRssi ? root.rssiToSignalLevel(rssiVal) : -1
534 property bool isPaired: root.isClassicMode && deviceAddress !== "" &&
535 root.subEditConfig && root.subEditConfig.isPaired(deviceAddress)
536
537 checkable: true
538 autoExclusive: true
539 checked: deviceAddress !== "" ? deviceAddress === root.currentAddress
540 : deviceName === (root.subEditConfig ? root.subEditConfig.deviceName : "")
541
542 contentItem: RowLayout {
543 spacing: root.screenTools.defaultFontPixelWidth
544
545 // Signal bars
546 Row {
547 id: deviceSignalBars
548 visible: deviceBtn.hasRssi
549 spacing: 1
550 readonly property real barWidth: root.screenTools.defaultFontPixelWidth * 0.4
551 readonly property real maxBarHeight: root.screenTools.defaultFontPixelHeight * 0.9
552
553 Repeater {
554 model: 4
555 delegate: Item {
556 id: deviceSignalBarDelegate
557 required property int index
558 width: deviceSignalBars.barWidth
559 height: deviceSignalBars.maxBarHeight
560
561 Rectangle {
562 width: parent.width
563 height: root.screenTools.defaultFontPixelHeight * (0.3 + deviceSignalBarDelegate.index * 0.2)
564 color: deviceSignalBarDelegate.index < deviceBtn.signalLevel ? root.palette.text : root.palette.buttonText
565 opacity: deviceSignalBarDelegate.index < deviceBtn.signalLevel ? 1.0 : 0.3
566 anchors.bottom: parent.bottom
567 }
568 }
569 }
570 }
571
572 QGCLabel {
573 text: deviceBtn.deviceName || deviceBtn.deviceAddress || qsTr("Unknown")
574 Layout.fillWidth: true
575 elide: Text.ElideRight
576 color: deviceBtn.checked ? root.palette.buttonHighlightText : root.palette.buttonText
577 }
578
579 QGCLabel {
580 visible: deviceBtn.isPaired
581 text: qsTr("Paired")
582 font.pointSize: root.screenTools.smallFontPointSize
583 color: root.palette.buttonText
584 }
585
586 QGCLabel {
587 visible: deviceBtn.hasRssi
588 text: deviceBtn.rssiVal + " dBm"
589 font.pointSize: root.screenTools.smallFontPointSize
590 color: root.palette.buttonText
591 }
592 }
593
594 onClicked: {
595 if (!root.subEditConfig) return
596 deviceAddress !== "" ? root.subEditConfig.setDeviceByAddress(deviceAddress)
597 : root.subEditConfig.setDevice(deviceName)
598 }
599 }
600 }
601 }
602 }
603
604 // Empty state
605 ColumnLayout {
606 Layout.fillWidth: true
607 spacing: root.screenTools.defaultFontPixelHeight
608 visible: !root.isScanning && root.sortedDevices.length === 0
609
610 QGCLabel {
611 text: qsTr("No devices found")
612 Layout.fillWidth: true
613 horizontalAlignment: Text.AlignHCenter
614 color: root.palette.warningText
615 }
616
617 QGCLabel {
618 text: root.isBleMode ? qsTr("Make sure your BLE device is powered on and advertising")
619 : qsTr("Make sure your Bluetooth device is powered on and discoverable")
620 Layout.fillWidth: true
621 horizontalAlignment: Text.AlignHCenter
622 wrapMode: Text.WordWrap
623 font.pointSize: root.screenTools.smallFontPointSize
624 color: root.palette.buttonText
625 }
626 }
627
628 // Scan button
629 QGCButton {
630 Layout.alignment: Qt.AlignHCenter
631 text: root.isScanning ? qsTr("Stop Scan") : qsTr("Scan for Devices")
632 onClicked: {
633 if (!root.subEditConfig) return
634 root.isScanning ? root.subEditConfig.stopScan() : root.subEditConfig.startScan()
635 }
636 }
637}