blob: 70b96ab73c596ec47f6b70edfbe177ee626db159 [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtBluetooth module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "osxbtledeviceinquiry_p.h"
#include "qbluetoothdeviceinfo.h"
#include "osxbtnotifier_p.h"
#include "qbluetoothuuid.h"
#include "osxbtutility_p.h"
#include <QtCore/qloggingcategory.h>
#include <QtCore/qdebug.h>
#include <QtCore/qendian.h>
#include <algorithm>
QT_BEGIN_NAMESPACE
namespace OSXBluetooth {
QBluetoothUuid qt_uuid(NSUUID *nsUuid)
{
if (!nsUuid)
return QBluetoothUuid();
uuid_t uuidData = {};
[nsUuid getUUIDBytes:uuidData];
quint128 qtUuidData = {};
std::copy(uuidData, uuidData + 16, qtUuidData.data);
return QBluetoothUuid(qtUuidData);
}
const int timeStepMS = 100;
const int powerOffTimeoutMS = 30000;
struct AdvertisementData {
// That's what CoreBluetooth has:
// CBAdvertisementDataLocalNameKey
// CBAdvertisementDataTxPowerLevelKey
// CBAdvertisementDataServiceUUIDsKey
// CBAdvertisementDataServiceDataKey
// CBAdvertisementDataManufacturerDataKey
// CBAdvertisementDataOverflowServiceUUIDsKey
// CBAdvertisementDataIsConnectable
// CBAdvertisementDataSolicitedServiceUUIDsKey
// For now, we "parse":
QString localName;
QVector<QBluetoothUuid> serviceUuids;
QHash<quint16, QByteArray> manufacturerData;
// TODO: other keys probably?
AdvertisementData(NSDictionary *AdvertisementData);
};
AdvertisementData::AdvertisementData(NSDictionary *advertisementData)
{
if (!advertisementData)
return;
// ... constant CBAdvertisementDataLocalNameKey ...
// NSString containing the local name of a peripheral.
NSObject *value = [advertisementData objectForKey:CBAdvertisementDataLocalNameKey];
if (value && [value isKindOfClass:[NSString class]])
localName = QString::fromNSString(static_cast<NSString *>(value));
// ... constant CBAdvertisementDataServiceUUIDsKey ...
// A list of one or more CBUUID objects, representing CBService UUIDs.
value = [advertisementData objectForKey:CBAdvertisementDataServiceUUIDsKey];
if (value && [value isKindOfClass:[NSArray class]]) {
NSArray *uuids = static_cast<NSArray *>(value);
for (CBUUID *cbUuid in uuids)
serviceUuids << qt_uuid(cbUuid);
}
value = [advertisementData objectForKey:CBAdvertisementDataManufacturerDataKey];
if (value && [value isKindOfClass:[NSData class]]) {
QByteArray data = QByteArray::fromNSData(static_cast<NSData *>(value));
manufacturerData.insert(qFromLittleEndian<quint16>(data.constData()), data.mid(2));
}
}
}
QT_END_NAMESPACE
QT_USE_NAMESPACE
@interface QT_MANGLE_NAMESPACE(OSXBTLEDeviceInquiry)(PrivateAPI)
- (void)stopScanSafe;
- (void)stopNotifier;
@end
@implementation QT_MANGLE_NAMESPACE(OSXBTLEDeviceInquiry)
{
LECBManagerNotifier *notifier;
ObjCScopedPointer<CBCentralManager> manager;
QList<QBluetoothDeviceInfo> devices;
LEInquiryState internalState;
int inquiryTimeoutMS;
QT_PREPEND_NAMESPACE(OSXBluetooth)::GCDTimer elapsedTimer;
}
-(id)initWithNotifier:(LECBManagerNotifier *)aNotifier
{
if (self = [super init]) {
Q_ASSERT(aNotifier);
notifier = aNotifier;
internalState = InquiryStarting;
inquiryTimeoutMS = OSXBluetooth::defaultLEScanTimeoutMS;
}
return self;
}
- (void)dealloc
{
[self stopScanSafe];
[manager setDelegate:nil];
[elapsedTimer cancelTimer];
[self stopNotifier];
[super dealloc];
}
- (void)timeout:(id)sender
{
Q_UNUSED(sender)
if (internalState == InquiryActive) {
[self stopScanSafe];
[manager setDelegate:nil];
internalState = InquiryFinished;
Q_ASSERT(notifier);
emit notifier->discoveryFinished();
} else if (internalState == InquiryStarting) {
// This is interesting on iOS only, where the system shows an alert
// asking to enable Bluetooth in the 'Settings' app. If not done yet
// (after 30 seconds) - we consider this as an error.
[manager setDelegate:nil];
internalState = ErrorPoweredOff;
Q_ASSERT(notifier);
emit notifier->CBManagerError(QBluetoothDeviceDiscoveryAgent::PoweredOffError);
}
}
- (void)startWithTimeout:(int)timeout
{
dispatch_queue_t leQueue(OSXBluetooth::qt_LE_queue());
Q_ASSERT(leQueue);
inquiryTimeoutMS = timeout;
manager.reset([[CBCentralManager alloc] initWithDelegate:self queue:leQueue]);
}
- (void)centralManagerDidUpdateState:(CBCentralManager *)central
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability-new"
if (central != manager)
return;
if (internalState != InquiryActive && internalState != InquiryStarting)
return;
Q_ASSERT(notifier);
using namespace OSXBluetooth;
const auto state = central.state;
#if QT_IOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__IPHONE_10_0) || QT_OSX_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_13)
if (state == CBManagerStatePoweredOn) {
#else
if (state == CBCentralManagerStatePoweredOn) {
#endif
if (internalState == InquiryStarting) {
internalState = InquiryActive;
if (inquiryTimeoutMS > 0) {
[elapsedTimer cancelTimer];
elapsedTimer.resetWithoutRetain([[GCDTimerObjC alloc] initWithDelegate:self]);
[elapsedTimer startWithTimeout:inquiryTimeoutMS step:timeStepMS];
}
[manager scanForPeripheralsWithServices:nil options:nil];
} // Else we ignore.
#if QT_IOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__IPHONE_10_0) || QT_OSX_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_13)
} else if (state == CBManagerStateUnsupported || state == CBManagerStateUnauthorized) {
#else
} else if (state == CBCentralManagerStateUnsupported || state == CBCentralManagerStateUnauthorized) {
#endif
if (internalState == InquiryActive) {
[self stopScanSafe];
// Not sure how this is possible at all,
// probably, can never happen.
internalState = ErrorPoweredOff;
emit notifier->CBManagerError(QBluetoothDeviceDiscoveryAgent::PoweredOffError);
} else {
internalState = ErrorLENotSupported;
emit notifier->LEnotSupported();
}
[manager setDelegate:nil];
#if QT_IOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__IPHONE_10_0) || QT_OSX_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_13)
} else if (state == CBManagerStatePoweredOff) {
#else
} else if (state == CBCentralManagerStatePoweredOff) {
#endif
#ifndef Q_OS_MACOS
if (internalState == InquiryStarting) {
// On iOS a user can see at this point an alert asking to
// enable Bluetooth in the "Settings" app. If a user does so,
// we'll receive 'PoweredOn' state update later.
// No change in internalState. Wait for 30 seconds.
[elapsedTimer cancelTimer];
elapsedTimer.resetWithoutRetain([[GCDTimerObjC alloc] initWithDelegate:self]);
[elapsedTimer startWithTimeout:powerOffTimeoutMS step:300];
return;
}
#else
Q_UNUSED(powerOffTimeoutMS)
#endif // Q_OS_MACOS
[elapsedTimer cancelTimer];
[self stopScanSafe];
[manager setDelegate:nil];
internalState = ErrorPoweredOff;
// On macOS we report PoweredOffError and our C++ owner will delete us
// (here we're kwnon as 'self'). Connection is Qt::QueuedConnection so we
// are apparently safe to call -stopNotifier after the signal.
emit notifier->CBManagerError(QBluetoothDeviceDiscoveryAgent::PoweredOffError);
[self stopNotifier];
} else {
// The following two states we ignore (from Apple's docs):
//"
// -CBCentralManagerStateUnknown
// The current state of the central manager is unknown;
// an update is imminent.
//
// -CBCentralManagerStateResetting
// The connection with the system service was momentarily
// lost; an update is imminent. "
// Wait for this imminent update.
}
#pragma clang diagnostic pop
}
- (void)stopScanSafe
{
// CoreBluetooth warns about API misused if we call stopScan in a state
// other than powered on. Hence this 'Safe' ...
if (!manager)
return;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability-new"
if (internalState == InquiryActive) {
const auto state = manager.data().state;
#if QT_IOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__IPHONE_10_0) || QT_OSX_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_13)
if (state == CBManagerStatePoweredOn)
#else
if (state == CBCentralManagerStatePoweredOn)
#endif
[manager stopScan];
}
#pragma clang diagnostic pop
}
- (void)stopNotifier
{
if (notifier) {
notifier->disconnect();
notifier->deleteLater();
notifier = nullptr;
}
}
- (void)stop
{
[self stopScanSafe];
[manager setDelegate:nil];
[elapsedTimer cancelTimer];
[self stopNotifier];
internalState = InquiryCancelled;
}
- (void)centralManager:(CBCentralManager *)central
didDiscoverPeripheral:(CBPeripheral *)peripheral
advertisementData:(NSDictionary *)advertisementData
RSSI:(NSNumber *)RSSI
{
using namespace OSXBluetooth;
if (central != manager)
return;
if (internalState != InquiryActive)
return;
if (!notifier)
return;
QBluetoothUuid deviceUuid;
if (!peripheral.identifier) {
qCWarning(QT_BT_OSX) << "peripheral without NSUUID";
return;
}
deviceUuid = OSXBluetooth::qt_uuid(peripheral.identifier);
if (deviceUuid.isNull()) {
qCWarning(QT_BT_OSX) << "no way to address peripheral, QBluetoothUuid is null";
return;
}
const AdvertisementData qtAdvData(advertisementData);
QString name(qtAdvData.localName);
if (!name.size() && peripheral.name)
name = QString::fromNSString(peripheral.name);
// TODO: fix 'classOfDevice' (0 for now).
QBluetoothDeviceInfo newDeviceInfo(deviceUuid, name, 0);
if (RSSI)
newDeviceInfo.setRssi([RSSI shortValue]);
if (qtAdvData.serviceUuids.size())
newDeviceInfo.setServiceUuids(qtAdvData.serviceUuids);
const QList<quint16> keys = qtAdvData.manufacturerData.keys();
for (quint16 key : keys)
newDeviceInfo.setManufacturerData(key, qtAdvData.manufacturerData.value(key));
// CoreBluetooth scans only for LE devices.
newDeviceInfo.setCoreConfigurations(QBluetoothDeviceInfo::LowEnergyCoreConfiguration);
emit notifier->deviceDiscovered(newDeviceInfo);
}
@end