Bluetooth Low Energy Scanner Example#
A Python application that demonstrates the analogous example in Qt Bluetooth Low Energy Scanner
 
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
"""PySide6 port of the bluetooth/lowenergyscanner example from Qt v6.x"""
import sys
from PySide6.QtCore import QCoreApplication
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine
from device import Device
from pathlib import Path
if __name__ == '__main__':
    app = QGuiApplication(sys.argv)
    engine = QQmlApplicationEngine()
    engine.addImportPath(Path(__file__).parent)
    engine.loadFromModule("Scanner", "Main")
    if not engine.rootObjects():
        sys.exit(-1)
    ex = QCoreApplication.exec()
    del engine
    sys.exit(ex)
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import warnings
from PySide6.QtBluetooth import (QBluetoothDeviceDiscoveryAgent, QLowEnergyController,
                                 QBluetoothDeviceInfo, QBluetoothUuid, QLowEnergyService)
from PySide6.QtCore import QObject, Property, Signal, Slot, QTimer, QMetaObject, Qt
from PySide6.QtQml import QmlElement, QmlSingleton
from deviceinfo import DeviceInfo
from serviceinfo import ServiceInfo
from characteristicinfo import CharacteristicInfo
QML_IMPORT_NAME = "Scanner"
QML_IMPORT_MAJOR_VERSION = 1
@QmlElement
@QmlSingleton
class Device(QObject):
    devices_updated = Signal()
    services_updated = Signal()
    characteristic_updated = Signal()
    update_changed = Signal()
    state_changed = Signal()
    disconnected = Signal()
    random_address_changed = Signal()
    def __init__(self, parent=None) -> None:
        super().__init__(parent)
        self.devices = []
        self._services = []
        self._characteristics = []
        self._previousAddress = ""
        self._message = ""
        self.currentDevice = DeviceInfo()
        self.connected = False
        self.controller: QLowEnergyController = None
        self._deviceScanState = False
        self.random_address = False
        self.discovery_agent = QBluetoothDeviceDiscoveryAgent()
        self.discovery_agent.setLowEnergyDiscoveryTimeout(25000)
        self.discovery_agent.deviceDiscovered.connect(self.add_device)
        self.discovery_agent.errorOccurred.connect(self.device_scan_error)
        self.discovery_agent.finished.connect(self.device_scan_finished)
        self.update = "Search"
    @Property("QVariant", notify=devices_updated)
    def devices_list(self):
        return self.devices
    @Property("QVariant", notify=services_updated)
    def services_list(self):
        return self._services
    @Property("QVariant", notify=characteristic_updated)
    def characteristic_list(self):
        return self._characteristics
    @Property(str, notify=update_changed)
    def update(self):
        return self._message
    @update.setter
    def update(self, message):
        self._message = message
        self.update_changed.emit()
    @Property(bool, notify=random_address_changed)
    def use_random_address(self):
        return self.random_address
    @use_random_address.setter
    def use_random_address(self, newValue):
        self.random_address = newValue
        self.random_address_changed.emit()
    @Property(bool, notify=state_changed)
    def state(self):
        return self._deviceScanState
    @Property(bool)
    def controller_error(self):
        return self.controller and (self.controller.error() != QLowEnergyController.NoError)
    @Slot()
    def start_device_discovery(self):
        self.devices.clear()
        self.devices_updated.emit()
        self.update = "Scanning for devices ..."
        self.discovery_agent.start(QBluetoothDeviceDiscoveryAgent.LowEnergyMethod)
        if self.discovery_agent.isActive():
            self._deviceScanState = True
            self.state_changed.emit()
    @Slot(str)
    def scan_services(self, address):
        # We need the current device for service discovery.
        for device in self.devices:
            if device.device_address == address:
                self.currentDevice.set_device(device.get_device())
                break
        if not self.currentDevice.get_device().isValid():
            warnings.warn("Not a valid device")
            return
        self._characteristics.clear()
        self.characteristic_updated.emit()
        self._services.clear()
        self.services_updated.emit()
        self.update = "Back\n(Connecting to device...)"
        if self.controller and (self._previousAddress != self.currentDevice.device_address):
            self.controller.disconnectFromDevice()
            del self.controller
            self.controller = None
        if not self.controller:
            self.controller = QLowEnergyController.createCentral(self.currentDevice.get_device())
            self.controller.connected.connect(self.device_connected)
            self.controller.errorOccurred.connect(self.error_received)
            self.controller.disconnected.connect(self.device_disconnected)
            self.controller.serviceDiscovered.connect(self.add_low_energy_service)
            self.controller.discoveryFinished.connect(self.services_scan_done)
        if self.random_address:
            self.controller.setRemoteAddressType(QLowEnergyController.RandomAddress)
        else:
            self.controller.setRemoteAddressType(QLowEnergyController.PublicAddress)
        self.controller.connectToDevice()
        self._previousAddress = self.currentDevice.device_address
    @Slot(str)
    def connect_to_service(self, uuid):
        service: QLowEnergyService = None
        for serviceInfo in self._services:
            if not serviceInfo:
                continue
            if serviceInfo.service_uuid == uuid:
                service = serviceInfo.service
                break
        if not service:
            return
        self._characteristics.clear()
        self.characteristic_updated.emit()
        if service.state() == QLowEnergyService.RemoteService:
            service.state_changed.connect(self.service_details_discovered)
            service.discoverDetails()
            self.update = "Back\n(Discovering details...)"
            return
        # discovery already done
        chars = service.characteristics()
        for ch in chars:
            cInfo = CharacteristicInfo(ch)
            self._characteristics.append(cInfo)
        QTimer.singleShot(0, self.characteristic_updated)
    @Slot()
    def disconnect_from_device(self):
        # UI always expects disconnect() signal when calling this signal
        # TODO what is really needed is to extend state() to a multi value
        # and thus allowing UI to keep track of controller progress in addition to
        # device scan progress
        if self.controller.state() != QLowEnergyController.UnconnectedState:
            self.controller.disconnectFromDevice()
        else:
            self.device_disconnected()
    @Slot(QBluetoothDeviceInfo)
    def add_device(self, info):
        if info.coreConfigurations() & QBluetoothDeviceInfo.LowEnergyCoreConfiguration:
            self.update = "Last device added: " + info.name()
    @Slot()
    def device_scan_finished(self):
        foundDevices = self.discovery_agent.discoveredDevices()
        for nextDevice in foundDevices:
            if nextDevice.coreConfigurations() & QBluetoothDeviceInfo.LowEnergyCoreConfiguration:
                device = DeviceInfo(nextDevice)
                self.devices.append(device)
        self.devices_updated.emit()
        self._deviceScanState = False
        self.state_changed.emit()
        if not self.devices:
            self.update = "No Low Energy devices found..."
        else:
            self.update = "Done! Scan Again!"
    @Slot("QBluetoothDeviceDiscovertAgent::Error")
    def device_scan_error(self, error):
        if error == QBluetoothDeviceDiscoveryAgent.PoweredOffError:
            self.update = (
                "The Bluetooth adaptor is powered off, power it on before doing discovery."
            )
        elif error == QBluetoothDeviceDiscoveryAgent.InputOutputError:
            self.update = "Writing or reading from the device resulted in an error."
        else:
            qme = self.discovery_agent.metaObject().enumerator(
                self.discovery_agent.metaObject().indexOfEnumerator("Error")
            )
            self.update = f"Error: {qme.valueToKey(error)}"
        self._deviceScanState = False
        self.devices_updated.emit()
        self.state_changed.emit()
    @Slot(QBluetoothUuid)
    def add_low_energy_service(self, service_uuid):
        service = self.controller.createServiceObject(service_uuid)
        if not service:
            warnings.warn("Cannot create service from uuid")
            return
        serv = ServiceInfo(service)
        self._services.append(serv)
        self.services_updated.emit()
    @Slot()
    def device_connected(self):
        self.update = "Back\n(Discovering services...)"
        self.connected = True
        self.controller.discoverServices()
    @Slot("QLowEnergyController::Error")
    def error_received(self, error):
        warnings.warn(f"Error: {self.controller.errorString()}")
        self.update = f"Back\n({self.controller.errorString()})"
    @Slot()
    def services_scan_done(self):
        self.update = "Back\n(Service scan done!)"
        # force UI in case we didn't find anything
        if not self._services:
            self.services_updated.emit()
    @Slot()
    def device_disconnected(self):
        warnings.warn("Disconnect from Device")
        self.disconnected.emit()
    @Slot("QLowEnergyService::ServiceState")
    def service_details_discovered(self, newState):
        if newState != QLowEnergyService.RemoteServiceDiscovered:
            # do not hang in "Scanning for characteristics" mode forever
            # in case the service discovery failed
            # We have to queue the signal up to give UI time to even enter
            # the above mode
            if newState != QLowEnergyService.RemoteServiceDiscovering:
                QMetaObject.invokeMethod(self.characteristic_updated, Qt.QueuedConnection)
            return
        service = self.sender()
        if not service:
            return
        chars = service.characteristics()
        for ch in chars:
            cInfo = CharacteristicInfo(ch)
            self._characteristics.append(cInfo)
        self.characteristic_updated.emit()
    @Slot()
    def stop_device_discovery(self):
        if self.discovery_agent.isActive():
            self.discovery_agent.stop()
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import sys
from PySide6.QtCore import QObject, Property, Signal
from PySide6.QtBluetooth import QBluetoothDeviceInfo
class DeviceInfo(QObject):
    device_changed = Signal()
    def __init__(self, d: QBluetoothDeviceInfo = None) -> None:
        super().__init__()
        self._device = d
    @Property(str, notify=device_changed)
    def device_name(self):
        return self._device.name()
    @Property(str, notify=device_changed)
    def device_address(self):
        if sys.platform == "darwin":
            return self._device.deviceUuid().toString()
        return self._device.address().toString()
    def get_device(self):
        return self._device
    def set_device(self, device):
        self._device = device
        self.device_changed.emit()
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from PySide6.QtCore import QObject, Property, Signal
from PySide6.QtBluetooth import QLowEnergyService
class ServiceInfo(QObject):
    service_changed = Signal()
    def __init__(self, service: QLowEnergyService) -> None:
        super().__init__()
        self._service = service
        self.service.setParent(self)
    @Property(str, notify=service_changed)
    def service_name(self):
        if not self.service:
            return ""
        return self.service.service_name()
    @Property(str, notify=service_changed)
    def service_type(self):
        if not self.service:
            return ""
        result = ""
        if (self.service.type() & QLowEnergyService.PrimaryService):
            result += "primary"
        else:
            result += "secondary"
        if (self.service.type() & QLowEnergyService.IncludedService):
            result += " included"
        result = '<' + result + '>'
        return result
    @Property(str, notify=service_changed)
    def service_uuid(self):
        if not self.service:
            return ""
        uuid = self.service.service_uuid()
        result16, success16 = uuid.toUInt16()
        if success16:
            return f"0x{result16:x}"
        result32, sucesss32 = uuid.toUInt32()
        if sucesss32:
            return f"0x{result32:x}"
        return uuid.toString().replace('{', '').replace('}', '')
    @property
    def service(self):
        return self._service
    @service.setter
    def service(self, service):
        self._service = service
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from PySide6.QtCore import QObject, Property, Signal
from PySide6.QtBluetooth import QLowEnergyCharacteristic, QBluetoothUuid
class CharacteristicInfo(QObject):
    characteristic_changed = Signal()
    def __init__(self, characteristic=None) -> None:
        super().__init__()
        self._characteristic = characteristic
    @Property(str, notify=characteristic_changed)
    def characteristic_name(self):
        if not self.characteristic:
            raise Exception("characteristic unset")
        name = self.characteristic.name()
        if name:
            return name
        for descriptor in self.characteristic.descriptors():
            if descriptor.type() == QBluetoothUuid.DescriptorType.CharacteristicUserDescription:
                name = descriptor.value()
                break
        if not name:
            name = "Unknown"
        return name
    @Property(str, notify=characteristic_changed)
    def characteristic_uuid(self):
        uuid = self.characteristic.uuid()
        result16, success16 = uuid.toUInt16()
        if success16:
            return f"0x{result16:x}"
        result32, sucess32 = uuid.toUInt32()
        if sucess32:
            return f"0x{result32:x}"
        return uuid.toString().replace('{', '').replace('}', '')
    @Property(str, notify=characteristic_changed)
    def characteristic_value(self):
        # Show raw string first and hex value below
        a = self.characteristic.value()
        if not a:
            return "<none>"
        result = f"{str(a)}\n{str(a.toHex())}"
        return result
    @Property(str, notify=characteristic_changed)
    def characteristic_permission(self):
        properties = "( "
        permission = self.characteristic.properties()
        if (permission & QLowEnergyCharacteristic.Read):
            properties += " Read"
        if (permission & QLowEnergyCharacteristic.Write):
            properties += " Write"
        if (permission & QLowEnergyCharacteristic.Notify):
            properties += " Notify"
        if (permission & QLowEnergyCharacteristic.Indicate):
            properties += " Indicate"
        if (permission & QLowEnergyCharacteristic.ExtendedProperty):
            properties += " ExtendedProperty"
        if (permission & QLowEnergyCharacteristic.Broadcasting):
            properties += " Broadcast"
        if (permission & QLowEnergyCharacteristic.WriteNoResponse):
            properties += " WriteNoResp"
        if (permission & QLowEnergyCharacteristic.WriteSigned):
            properties += " WriteSigned"
        properties += " )"
        return properties
    @property
    def characteristic(self):
        return self._characteristic
    @characteristic.setter
    def characteristic(self, characteristic):
        self._characteristic = characteristic
        self.characteristic_changed.emit()
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Layouts
Window {
    id: main
    width: 300
    height: 600
    visible: true
    StackLayout {
        id: pagesLayout
        anchors.fill: parent
        currentIndex: 0
        Devices {
            onShowServices: pagesLayout.currentIndex = 1
        }
        Services {
            onShowDevices: pagesLayout.currentIndex = 0
            onShowCharacteristics: pagesLayout.currentIndex = 2
        }
        Characteristics {
            onShowDevices: pagesLayout.currentIndex = 0
            onShowServices: pagesLayout.currentIndex = 1
        }
    }
}
// Copyright (C) 2013 BlackBerry Limited. All rights reserved.
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
Rectangle {
    id: menu
    property real menuWidth: 100
    property real menuHeight: 50
    property string menuText: "Search"
    signal buttonClick
    height: menuHeight
    width: menuWidth
    Rectangle {
        id: search
        width: parent.width
        height: parent.height
        anchors.centerIn: parent
        color: "#363636"
        border.width: 1
        border.color: "#E3E3E3"
        radius: 5
        Text {
            id: searchText
            horizontalAlignment: Text.AlignHCenter
            verticalAlignment: Text.AlignVCenter
            anchors.fill: parent
            text: menu.menuText
            elide: Text.ElideMiddle
            color: "#E3E3E3"
            wrapMode: Text.WordWrap
        }
        MouseArea {
            anchors.fill: parent
            onPressed: {
                search.width = search.width - 7
                search.height = search.height - 5
            }
            onReleased: {
                search.width = search.width + 7
                search.height = search.height + 5
            }
            onClicked: {
                menu.buttonClick()
            }
        }
    }
}
// Copyright (C) 2013 BlackBerry Limited. All rights reserved.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
Rectangle {
    id: header
    width: parent.width
    height: 70
    border.width: 1
    border.color: "#363636"
    radius: 5
    property string headerText: ""
    Text {
        horizontalAlignment: Text.AlignHCenter
        verticalAlignment: Text.AlignVCenter
        anchors.fill: parent
        text: header.headerText
        font.bold: true
        font.pointSize: 20
        elide: Text.ElideMiddle
        color: "#363636"
    }
}
// Copyright (C) 2013 BlackBerry Limited. All rights reserved.
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
pragma ComponentBehavior: Bound
import QtQuick
Rectangle {
    id: characteristicsPage
    signal showServices
    signal showDevices
    width: 300
    height: 600
    Header {
        id: header
        anchors.top: parent.top
        headerText: "Characteristics list"
    }
    Dialog {
        id: info
        anchors.centerIn: parent
        visible: true
        dialogText: "Scanning for characteristics..."
    }
    Connections {
        target: Device
        function oncharacteristics_pdated() {
            menu.menuText = "Back"
            if (characteristicview.count === 0) {
                info.dialogText = "No characteristic found"
                info.busyImage = false
            } else {
                info.visible = false
                info.busyImage = true
            }
        }
        function onDisconnected() {
            characteristicsPage.showDevices()
        }
    }
    ListView {
        id: characteristicview
        width: parent.width
        clip: true
        anchors.top: header.bottom
        anchors.bottom: menu.top
        model: Device.characteristicList
        delegate: Rectangle {
            required property var modelData
            id: box
            height: 300
            width: characteristicview.width
            color: "lightsteelblue"
            border.width: 2
            border.color: "black"
            radius: 5
            Label {
                id: characteristicName
                textContent: box.modelData.characteristic_name
                anchors.top: parent.top
                anchors.topMargin: 5
            }
            Label {
                id: characteristicUuid
                font.pointSize: characteristicName.font.pointSize * 0.7
                textContent: box.modelData.characteristic_uuid
                anchors.top: characteristicName.bottom
                anchors.topMargin: 5
            }
            Label {
                id: characteristicValue
                font.pointSize: characteristicName.font.pointSize * 0.7
                textContent: ("Value: " + box.modelData.characteristic_value)
                anchors.bottom: characteristicHandle.top
                horizontalAlignment: Text.AlignHCenter
                anchors.topMargin: 5
            }
            Label {
                id: characteristicHandle
                font.pointSize: characteristicName.font.pointSize * 0.7
                textContent: ("Handlers: " + box.modelData.characteristic_handle)
                anchors.bottom: characteristicPermission.top
                anchors.topMargin: 5
            }
            Label {
                id: characteristicPermission
                font.pointSize: characteristicName.font.pointSize * 0.7
                textContent: box.modelData.characteristic_permission
                anchors.bottom: parent.bottom
                anchors.topMargin: 5
                anchors.bottomMargin: 5
            }
        }
    }
    Menu {
        id: menu
        anchors.bottom: parent.bottom
        menuWidth: parent.width
        menuText: Device.update
        menuHeight: (parent.height / 6)
        onButtonClick: {
            characteristicsPage.showServices()
            Device.update = "Back"
        }
    }
}
// Copyright (C) 2013 BlackBerry Limited. All rights reserved.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
Rectangle {
    id: dialog
    width: parent.width / 3 * 2
    height: dialogTextId.height + background.height + 20
    z: 50
    property string dialogText: ""
    property bool busyImage: true
    border.width: 1
    border.color: "#363636"
    radius: 10
    Text {
        id: dialogTextId
        horizontalAlignment: Text.AlignHCenter
        verticalAlignment: Text.AlignVCenter
        anchors.horizontalCenter: parent.horizontalCenter
        anchors.top: parent.top
        anchors.topMargin: 10
        elide: Text.ElideMiddle
        text: dialog.dialogText
        color: "#363636"
        wrapMode: Text.Wrap
    }
    Image {
        id: background
        width: 20
        height: 20
        anchors.top: dialogTextId.bottom
        anchors.horizontalCenter: dialogTextId.horizontalCenter
        visible: parent.busyImage
        source: "assets/busy_dark.png"
        fillMode: Image.PreserveAspectFit
        NumberAnimation on rotation {
            duration: 3000
            from: 0
            to: 360
            loops: Animation.Infinite
        }
    }
}
// Copyright (C) 2013 BlackBerry Limited. All rights reserved.
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
pragma ComponentBehavior: Bound
import QtQuick
Rectangle {
    id: servicesPage
    signal showCharacteristics
    signal showDevices
    width: 300
    height: 600
    Component.onCompleted: {
        // Loading this page may take longer than QLEController
        // stopping with an error, go back and readjust this view
        // based on controller errors
        if (Device.controller_error) {
            info.visible = false
            menu.menuText = Device.update
        }
    }
    Header {
        id: header
        anchors.top: parent.top
        headerText: "Services list"
    }
    Dialog {
        id: info
        anchors.centerIn: parent
        visible: true
        dialogText: "Scanning for services..."
    }
    Connections {
        target: Device
        function onservices_updated() {
            if (servicesview.count === 0)
                info.dialogText = "No services found"
            else
                info.visible = false
        }
        function ondisconnected() {
            servicesPage.showDevices()
        }
    }
    ListView {
        id: servicesview
        width: parent.width
        anchors.top: header.bottom
        anchors.bottom: menu.top
        model: Device.servicesList
        clip: true
        delegate: Rectangle {
            required property var modelData
            id: box
            height: 100
            color: "lightsteelblue"
            border.width: 2
            border.color: "black"
            radius: 5
            width: servicesview.width
            MouseArea {
                anchors.fill: parent
                onClicked: {
                    Device.connectToService(box.modelData.service_uuid)
                    servicesPage.showCharacteristics()
                }
            }
            Label {
                id: serviceName
                textContent: box.modelData.service_name
                anchors.top: parent.top
                anchors.topMargin: 5
            }
            Label {
                textContent: box.modelData.service_type
                font.pointSize: serviceName.font.pointSize * 0.5
                anchors.top: serviceName.bottom
            }
            Label {
                id: serviceUuid
                font.pointSize: serviceName.font.pointSize * 0.5
                textContent: box.modelData.service_uuid
                anchors.bottom: box.bottom
                anchors.bottomMargin: 5
            }
        }
    }
    Menu {
        id: menu
        anchors.bottom: parent.bottom
        menuWidth: parent.width
        menuText: Device.update
        menuHeight: (parent.height / 6)
        onButtonClick: {
            Device.disconnect_from_device()
            servicesPage.showDevices()
            Device.update = "Search"
        }
    }
}
// Copyright (C) 2013 BlackBerry Limited. All rights reserved.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
Text {
    property string textContent: ""
    font.pointSize: 20
    anchors.horizontalCenter: parent.horizontalCenter
    color: "#363636"
    horizontalAlignment: Text.AlignHCenter
    elide: Text.ElideMiddle
    width: parent.width
    wrapMode: Text.Wrap
    text: textContent
}
// Copyright (C) 2013 BlackBerry Limited. All rights reserved.
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
pragma ComponentBehavior: Bound
import QtQuick
Rectangle {
    id: devicesPage
    property bool deviceState: Device.state
    signal showServices
    width: 300
    height: 600
    onDeviceStateChanged: {
        if (!Device.state)
            info.visible = false
    }
    Header {
        id: header
        anchors.top: parent.top
        headerText: {
            if (Device.state)
                return "Discovering"
            if (Device.devices_list.length > 0)
                return "Select a device"
            return "Start Discovery"
        }
    }
    Dialog {
        id: info
        anchors.centerIn: parent
        visible: false
    }
    ListView {
        id: theListView
        width: parent.width
        clip: true
        anchors.top: header.bottom
        anchors.bottom: connectToggle.top
        model: Device.devices_list
        delegate: Rectangle {
            required property var modelData
            id: box
            height: 100
            width: theListView.width
            color: "lightsteelblue"
            border.width: 2
            border.color: "black"
            radius: 5
            MouseArea {
                anchors.fill: parent
                onClicked: {
                    Device.scan_services(box.modelData.device_address)
                    showServices()
                }
            }
            Label {
                id: deviceName
                textContent: box.modelData.device_name
                anchors.top: parent.top
                anchors.topMargin: 5
            }
            Label {
                id: deviceAddress
                textContent: box.modelData.device_address
                font.pointSize: deviceName.font.pointSize * 0.7
                anchors.bottom: box.bottom
                anchors.bottomMargin: 5
            }
        }
    }
    Menu {
        id: connectToggle
        menuWidth: parent.width
        anchors.bottom: menu.top
        menuText: {
            visible = Device.devices_list.length > 0
            if (Device.use_random_address)
                return "Address type: Random"
            else
                return "Address type: Public"
        }
        onButtonClick: Device.use_random_address = !Device.use_random_address
    }
    Menu {
        id: menu
        anchors.bottom: parent.bottom
        menuWidth: parent.width
        menuHeight: (parent.height / 6)
        menuText: Device.update
        onButtonClick: {
            if (!Device.state) {
                Device.start_device_discovery()
                // if start_device_discovery() failed Device.state is not set
                if (Device.state) {
                    info.dialogText = "Searching..."
                    info.visible = true
                }
            } else {
                Device.stop_device_discovery()
            }
        }
    }
}
module Scanner
typeinfo scanner.qmltypes
Characteristics 1.0 Characteristics.qml
Devices 1.0 Devices.qml
Dialog 1.0 Dialog.qml
Header 1.0 Header.qml
Label 1.0 Label.qml
Main 1.0 Main.qml
Menu 1.0 Menu.qml
Services 1.0 Services.qml