1pragma ComponentBehavior: Bound
8import QGroundControl.Controls
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
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)
25 spacing: root._rowSpacing
27 visible: root.subEditConfig !== null
29 function saveSettings() { }
32 property bool paired: false
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
41 property var knownDevices: []
42 property var availableAdapters: []
43 property bool _initialAdapterSelected: false
45 function toArray(listLike) {
49 if (Array.isArray(listLike)) {
50 return listLike.slice()
55 if (typeof listLike.length === "number") {
56 length = listLike.length
57 for (var i = 0; i < length; i++) {
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))
73 readonly property var sortedDevices: {
74 if (!root.subEditConfig) return []
75 var arr = root.toArray(root.subEditConfig.devicesModel)
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
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)
93 //-- Helper Functions --
94 function refreshDeviceLists() {
95 if (!root.subEditConfig) {
96 root.knownDevices = []
97 root.availableAdapters = []
98 _initialAdapterSelected = false
101 var connected = root.subEditConfig.getConnectedDevices() || []
102 var pairedDevices = root.subEditConfig.getAllPairedDevices() || []
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 })
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 })
120 root.knownDevices = merged
121 root.availableAdapters = root.subEditConfig.getAllAvailableAdapters() || []
123 if (root.subEditConfig.adapterAddress && root.subEditConfig.adapterAddress !== "") {
124 _initialAdapterSelected = true
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)
136 function updatePairingStatus() {
137 if (!root.subEditConfig) {
141 root.paired = root.isClassicMode && root.currentAddress !== "" &&
142 root.subEditConfig.isPaired(root.currentAddress)
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
153 //-- Signal Connections --
155 target: root.subEditConfig
156 enabled: root.subEditConfig !== null
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) }
166 Component.onCompleted: {
168 updatePairingStatus()
171 onSubEditConfigChanged: {
172 _initialAdapterSelected = false
174 updatePairingStatus()
177 //==========================================================================
178 //-- Bluetooth Adapter Section --
179 //==========================================================================
181 Layout.fillWidth: true
182 text: qsTr("Bluetooth Adapter")
187 columnSpacing: root._colSpacing
188 rowSpacing: root._rowSpacing
189 Layout.fillWidth: true
192 text: qsTr("Adapter")
193 visible: root.hasAdapter
197 Layout.preferredWidth: root._secondColumnWidth
198 visible: root.hasAdapter
199 sizeToContents: false
202 // Column is ~30 chars, MAC + parens = 20 chars, leaves ~10 for name
203 readonly property int maxNameLength: 10
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) + "…"
213 result.push(name + " (" + a.address + ")")
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
224 onActivated: function(index) {
225 if (root.subEditConfig && index >= 0 && index < root.availableAdapters.length) {
226 root.subEditConfig.selectAdapter(root.availableAdapters[index].address)
233 visible: root.hasAdapter
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
243 text: qsTr("Bluetooth adapter unavailable")
244 visible: !root.hasAdapter
245 color: root.palette.warningText
251 Layout.fillWidth: true
252 spacing: root._colSpacing
253 visible: root.hasAdapter
256 text: qsTr("Powered On")
257 checked: root.adapterOn
259 if (!root.subEditConfig) return
260 root.adapterOn ? root.subEditConfig.powerOffAdapter() : root.subEditConfig.powerOnAdapter()
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) }
273 //==========================================================================
274 //-- Connection Settings Section --
275 //==========================================================================
277 Layout.fillWidth: true
278 text: qsTr("Connection")
283 columnSpacing: root._colSpacing
284 rowSpacing: root._rowSpacing
285 Layout.fillWidth: true
287 QGCLabel { text: qsTr("Mode") }
289 Layout.preferredWidth: root._secondColumnWidth
290 spacing: root._colSpacing
293 text: qsTr("Classic")
294 checked: root.isClassicMode
295 onClicked: { if (root.subEditConfig) root.subEditConfig.mode = root.btConfig.BluetoothMode.ModeClassic }
300 checked: root.isBleMode
301 onClicked: { if (root.subEditConfig) root.subEditConfig.mode = root.btConfig.BluetoothMode.ModeLowEnergy }
305 QGCLabel { text: qsTr("Selected Device") }
307 Layout.preferredWidth: root._secondColumnWidth
308 text: root.subEditConfig && root.subEditConfig.deviceName ? root.subEditConfig.deviceName : qsTr("None")
311 QGCLabel { text: qsTr("Device Address") }
313 Layout.preferredWidth: root._secondColumnWidth
314 text: root.currentAddress || qsTr("N/A")
317 // Classic Bluetooth Pairing
319 text: qsTr("Pairing")
320 visible: root.isClassicMode && root.currentAddress !== ""
323 Layout.preferredWidth: root._secondColumnWidth
324 visible: root.isClassicMode && root.currentAddress !== ""
325 spacing: root._colSpacing
328 text: root.subEditConfig ? root.subEditConfig.getPairingStatus(root.currentAddress) : ""
329 Layout.fillWidth: true
333 text: root.paired ? qsTr("Unpair") : qsTr("Pair")
335 if (!root.subEditConfig) return
336 root.paired ? root.subEditConfig.removePairing(root.currentAddress)
337 : root.subEditConfig.requestPairing(root.currentAddress)
342 // BLE Signal Strength
344 text: qsTr("Signal Strength")
345 visible: root.isBleMode && (rssiDisplay.hasConnected || rssiDisplay.hasSelected)
349 Layout.preferredWidth: root._secondColumnWidth
350 visible: root.isBleMode && (hasConnected || hasSelected)
351 spacing: root.screenTools.defaultFontPixelWidth
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)
361 readonly property real barWidth: root.screenTools.defaultFontPixelWidth * 0.5
362 readonly property real maxBarHeight: root.screenTools.defaultFontPixelHeight
368 required property int index
369 width: rssiBars.barWidth
370 height: rssiBars.maxBarHeight
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
384 text: rssiDisplay.rssi + " dBm"
385 color: rssiDisplay.hasConnected ? root.palette.text : root.palette.buttonText
389 text: rssiDisplay.hasConnected ? qsTr("(Connected)") : qsTr("(Last Scan)")
390 font.pointSize: root.screenTools.smallFontPointSize
391 color: root.palette.buttonText
396 //==========================================================================
397 //-- BLE Configuration Section (Collapsible) --
398 //==========================================================================
401 text: qsTr("Advanced BLE Configuration")
402 visible: root.isBleMode
408 columnSpacing: root._colSpacing
409 rowSpacing: root._rowSpacing
410 Layout.fillWidth: true
411 visible: root.isBleMode && showBleConfig.checked
413 QGCLabel { text: qsTr("Service UUID") }
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 }
422 QGCLabel { text: qsTr("RX Characteristic") }
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 }
431 QGCLabel { text: qsTr("TX Characteristic") }
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 }
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
450 //==========================================================================
451 //-- Known Devices Section (Classic only) --
452 //==========================================================================
454 Layout.fillWidth: true
455 text: qsTr("Known Devices")
456 visible: root.isClassicMode && root.knownDevices.length > 0
460 Layout.fillWidth: true
461 spacing: root.screenTools.defaultFontPixelWidth
462 visible: root.isClassicMode && root.knownDevices.length > 0
465 model: root.knownDevices
466 delegate: QGCButton {
467 required property var modelData
468 property var dev: modelData
469 property bool isConnected: dev.isConnected === true
471 text: (dev.name || dev.address || qsTr("Unknown")) + (isConnected ? " [Connected]" : "")
473 checked: dev.address === root.currentAddress
474 onClicked: { if (root.subEditConfig && dev.address) root.subEditConfig.setDeviceByAddress(dev.address) }
479 //==========================================================================
480 //-- Available Devices Section --
481 //==========================================================================
483 Layout.fillWidth: true
484 text: root.isBleMode ? qsTr("Available BLE Devices") : qsTr("Available Devices")
489 Layout.fillWidth: true
490 visible: root.isScanning
491 spacing: root.screenTools.defaultFontPixelWidth
494 Layout.preferredWidth: root.screenTools.defaultFontPixelHeight * 2
495 Layout.preferredHeight: Layout.preferredWidth
500 text: qsTr("Scanning for devices...")
501 color: root.palette.text
508 Layout.fillWidth: true
509 Layout.preferredHeight: Math.min(deviceList.contentHeight, root.screenTools.defaultFontPixelHeight * 16)
510 contentHeight: deviceColumn.height
511 contentWidth: root.width
513 visible: !root.isScanning || root.sortedDevices.length > 0
518 spacing: root.screenTools.defaultFontPixelHeight * 0.25
522 model: root.sortedDevices
523 delegate: QGCButton {
525 required property var modelData
526 Layout.fillWidth: true
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)
539 checked: deviceAddress !== "" ? deviceAddress === root.currentAddress
540 : deviceName === (root.subEditConfig ? root.subEditConfig.deviceName : "")
542 contentItem: RowLayout {
543 spacing: root.screenTools.defaultFontPixelWidth
548 visible: deviceBtn.hasRssi
550 readonly property real barWidth: root.screenTools.defaultFontPixelWidth * 0.4
551 readonly property real maxBarHeight: root.screenTools.defaultFontPixelHeight * 0.9
556 id: deviceSignalBarDelegate
557 required property int index
558 width: deviceSignalBars.barWidth
559 height: deviceSignalBars.maxBarHeight
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
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
580 visible: deviceBtn.isPaired
582 font.pointSize: root.screenTools.smallFontPointSize
583 color: root.palette.buttonText
587 visible: deviceBtn.hasRssi
588 text: deviceBtn.rssiVal + " dBm"
589 font.pointSize: root.screenTools.smallFontPointSize
590 color: root.palette.buttonText
595 if (!root.subEditConfig) return
596 deviceAddress !== "" ? root.subEditConfig.setDeviceByAddress(deviceAddress)
597 : root.subEditConfig.setDevice(deviceName)
606 Layout.fillWidth: true
607 spacing: root.screenTools.defaultFontPixelHeight
608 visible: !root.isScanning && root.sortedDevices.length === 0
611 text: qsTr("No devices found")
612 Layout.fillWidth: true
613 horizontalAlignment: Text.AlignHCenter
614 color: root.palette.warningText
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
630 Layout.alignment: Qt.AlignHCenter
631 text: root.isScanning ? qsTr("Stop Scan") : qsTr("Scan for Devices")
633 if (!root.subEditConfig) return
634 root.isScanning ? root.subEditConfig.stopScan() : root.subEditConfig.startScan()