Bluetooth Low Energy Scanner Example

Tags: Android

A Python application that demonstrates the analogous example in Qt Bluetooth Low Energy Scanner

lowenergyscanner screenshot

Download this example

# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

"""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  # noqa: F401
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)

    exit_code = QCoreApplication.exec()
    del engine
    sys.exit(exit_code)
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations
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
from __future__ import annotations

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 __future__ import annotations

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 __future__ import annotations

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