1 | /*************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2013 BlackBerry Limited. All rights reserved. |
4 | ** Copyright (C) 2017 The Qt Company Ltd. |
5 | ** Contact: https://www.qt.io/licensing/ |
6 | ** |
7 | ** This file is part of the QtBluetooth module of the Qt Toolkit. |
8 | ** |
9 | ** $QT_BEGIN_LICENSE:BSD$ |
10 | ** Commercial License Usage |
11 | ** Licensees holding valid commercial Qt licenses may use this file in |
12 | ** accordance with the commercial license agreement provided with the |
13 | ** Software or, alternatively, in accordance with the terms contained in |
14 | ** a written agreement between you and The Qt Company. For licensing terms |
15 | ** and conditions see https://www.qt.io/terms-conditions. For further |
16 | ** information use the contact form at https://www.qt.io/contact-us. |
17 | ** |
18 | ** BSD License Usage |
19 | ** Alternatively, you may use this file under the terms of the BSD license |
20 | ** as follows: |
21 | ** |
22 | ** "Redistribution and use in source and binary forms, with or without |
23 | ** modification, are permitted provided that the following conditions are |
24 | ** met: |
25 | ** * Redistributions of source code must retain the above copyright |
26 | ** notice, this list of conditions and the following disclaimer. |
27 | ** * Redistributions in binary form must reproduce the above copyright |
28 | ** notice, this list of conditions and the following disclaimer in |
29 | ** the documentation and/or other materials provided with the |
30 | ** distribution. |
31 | ** * Neither the name of The Qt Company Ltd nor the names of its |
32 | ** contributors may be used to endorse or promote products derived |
33 | ** from this software without specific prior written permission. |
34 | ** |
35 | ** |
36 | ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
37 | ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
38 | ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
39 | ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
40 | ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
41 | ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
42 | ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
43 | ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
44 | ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
45 | ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
46 | ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." |
47 | ** |
48 | ** $QT_END_LICENSE$ |
49 | ** |
50 | ****************************************************************************/ |
51 | |
52 | #include "device.h" |
53 | |
54 | #include <qbluetoothaddress.h> |
55 | #include <qbluetoothdevicediscoveryagent.h> |
56 | #include <qbluetoothlocaldevice.h> |
57 | #include <qbluetoothdeviceinfo.h> |
58 | #include <qbluetoothservicediscoveryagent.h> |
59 | #include <QDebug> |
60 | #include <QList> |
61 | #include <QMetaEnum> |
62 | #include <QTimer> |
63 | |
64 | Device::Device() |
65 | { |
66 | //! [les-devicediscovery-1] |
67 | discoveryAgent = new QBluetoothDeviceDiscoveryAgent(); |
68 | discoveryAgent->setLowEnergyDiscoveryTimeout(5000); |
69 | connect(sender: discoveryAgent, signal: &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, |
70 | receiver: this, slot: &Device::addDevice); |
71 | connect(sender: discoveryAgent, signal: QOverload<QBluetoothDeviceDiscoveryAgent::Error>::of(ptr: &QBluetoothDeviceDiscoveryAgent::error), |
72 | receiver: this, slot: &Device::deviceScanError); |
73 | connect(sender: discoveryAgent, signal: &QBluetoothDeviceDiscoveryAgent::finished, receiver: this, slot: &Device::deviceScanFinished); |
74 | //! [les-devicediscovery-1] |
75 | |
76 | setUpdate("Search" ); |
77 | } |
78 | |
79 | Device::~Device() |
80 | { |
81 | delete discoveryAgent; |
82 | delete controller; |
83 | qDeleteAll(c: devices); |
84 | qDeleteAll(c: m_services); |
85 | qDeleteAll(c: m_characteristics); |
86 | devices.clear(); |
87 | m_services.clear(); |
88 | m_characteristics.clear(); |
89 | } |
90 | |
91 | void Device::startDeviceDiscovery() |
92 | { |
93 | qDeleteAll(c: devices); |
94 | devices.clear(); |
95 | emit devicesUpdated(); |
96 | |
97 | setUpdate("Scanning for devices ..." ); |
98 | //! [les-devicediscovery-2] |
99 | discoveryAgent->start(method: QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); |
100 | //! [les-devicediscovery-2] |
101 | |
102 | if (discoveryAgent->isActive()) { |
103 | m_deviceScanState = true; |
104 | Q_EMIT stateChanged(); |
105 | } |
106 | } |
107 | |
108 | //! [les-devicediscovery-3] |
109 | void Device::addDevice(const QBluetoothDeviceInfo &info) |
110 | { |
111 | if (info.coreConfigurations() & QBluetoothDeviceInfo::LowEnergyCoreConfiguration) |
112 | setUpdate("Last device added: " + info.name()); |
113 | } |
114 | //! [les-devicediscovery-3] |
115 | |
116 | void Device::deviceScanFinished() |
117 | { |
118 | const QList<QBluetoothDeviceInfo> foundDevices = discoveryAgent->discoveredDevices(); |
119 | for (auto nextDevice : foundDevices) |
120 | if (nextDevice.coreConfigurations() & QBluetoothDeviceInfo::LowEnergyCoreConfiguration) |
121 | devices.append(t: new DeviceInfo(nextDevice)); |
122 | |
123 | emit devicesUpdated(); |
124 | m_deviceScanState = false; |
125 | emit stateChanged(); |
126 | if (devices.isEmpty()) |
127 | setUpdate("No Low Energy devices found..." ); |
128 | else |
129 | setUpdate("Done! Scan Again!" ); |
130 | } |
131 | |
132 | QVariant Device::getDevices() |
133 | { |
134 | return QVariant::fromValue(value: devices); |
135 | } |
136 | |
137 | QVariant Device::getServices() |
138 | { |
139 | return QVariant::fromValue(value: m_services); |
140 | } |
141 | |
142 | QVariant Device::getCharacteristics() |
143 | { |
144 | return QVariant::fromValue(value: m_characteristics); |
145 | } |
146 | |
147 | QString Device::getUpdate() |
148 | { |
149 | return m_message; |
150 | } |
151 | |
152 | void Device::scanServices(const QString &address) |
153 | { |
154 | // We need the current device for service discovery. |
155 | |
156 | for (auto d: qAsConst(t&: devices)) { |
157 | if (auto device = qobject_cast<DeviceInfo *>(object: d)) { |
158 | if (device->getAddress() == address ) { |
159 | currentDevice.setDevice(device->getDevice()); |
160 | break; |
161 | } |
162 | } |
163 | } |
164 | |
165 | if (!currentDevice.getDevice().isValid()) { |
166 | qWarning() << "Not a valid device" ; |
167 | return; |
168 | } |
169 | |
170 | qDeleteAll(c: m_characteristics); |
171 | m_characteristics.clear(); |
172 | emit characteristicsUpdated(); |
173 | qDeleteAll(c: m_services); |
174 | m_services.clear(); |
175 | emit servicesUpdated(); |
176 | |
177 | setUpdate("Back\n(Connecting to device...)" ); |
178 | |
179 | if (controller && m_previousAddress != currentDevice.getAddress()) { |
180 | controller->disconnectFromDevice(); |
181 | delete controller; |
182 | controller = nullptr; |
183 | } |
184 | |
185 | //! [les-controller-1] |
186 | if (!controller) { |
187 | // Connecting signals and slots for connecting to LE services. |
188 | controller = QLowEnergyController::createCentral(remoteDevice: currentDevice.getDevice()); |
189 | connect(sender: controller, signal: &QLowEnergyController::connected, |
190 | receiver: this, slot: &Device::deviceConnected); |
191 | connect(sender: controller, signal: QOverload<QLowEnergyController::Error>::of(ptr: &QLowEnergyController::error), |
192 | receiver: this, slot: &Device::errorReceived); |
193 | connect(sender: controller, signal: &QLowEnergyController::disconnected, |
194 | receiver: this, slot: &Device::deviceDisconnected); |
195 | connect(sender: controller, signal: &QLowEnergyController::serviceDiscovered, |
196 | receiver: this, slot: &Device::addLowEnergyService); |
197 | connect(sender: controller, signal: &QLowEnergyController::discoveryFinished, |
198 | receiver: this, slot: &Device::serviceScanDone); |
199 | } |
200 | |
201 | if (isRandomAddress()) |
202 | controller->setRemoteAddressType(QLowEnergyController::RandomAddress); |
203 | else |
204 | controller->setRemoteAddressType(QLowEnergyController::PublicAddress); |
205 | controller->connectToDevice(); |
206 | //! [les-controller-1] |
207 | |
208 | m_previousAddress = currentDevice.getAddress(); |
209 | } |
210 | |
211 | void Device::addLowEnergyService(const QBluetoothUuid &serviceUuid) |
212 | { |
213 | //! [les-service-1] |
214 | QLowEnergyService *service = controller->createServiceObject(service: serviceUuid); |
215 | if (!service) { |
216 | qWarning() << "Cannot create service for uuid" ; |
217 | return; |
218 | } |
219 | //! [les-service-1] |
220 | auto serv = new ServiceInfo(service); |
221 | m_services.append(t: serv); |
222 | |
223 | emit servicesUpdated(); |
224 | } |
225 | //! [les-service-1] |
226 | |
227 | void Device::serviceScanDone() |
228 | { |
229 | setUpdate("Back\n(Service scan done!)" ); |
230 | // force UI in case we didn't find anything |
231 | if (m_services.isEmpty()) |
232 | emit servicesUpdated(); |
233 | } |
234 | |
235 | void Device::connectToService(const QString &uuid) |
236 | { |
237 | QLowEnergyService *service = nullptr; |
238 | for (auto s: qAsConst(t&: m_services)) { |
239 | auto serviceInfo = qobject_cast<ServiceInfo *>(object: s); |
240 | if (!serviceInfo) |
241 | continue; |
242 | |
243 | if (serviceInfo->getUuid() == uuid) { |
244 | service = serviceInfo->service(); |
245 | break; |
246 | } |
247 | } |
248 | |
249 | if (!service) |
250 | return; |
251 | |
252 | qDeleteAll(c: m_characteristics); |
253 | m_characteristics.clear(); |
254 | emit characteristicsUpdated(); |
255 | |
256 | if (service->state() == QLowEnergyService::DiscoveryRequired) { |
257 | //! [les-service-3] |
258 | connect(sender: service, signal: &QLowEnergyService::stateChanged, |
259 | receiver: this, slot: &Device::serviceDetailsDiscovered); |
260 | service->discoverDetails(); |
261 | setUpdate("Back\n(Discovering details...)" ); |
262 | //! [les-service-3] |
263 | return; |
264 | } |
265 | |
266 | //discovery already done |
267 | const QList<QLowEnergyCharacteristic> chars = service->characteristics(); |
268 | for (const QLowEnergyCharacteristic &ch : chars) { |
269 | auto cInfo = new CharacteristicInfo(ch); |
270 | m_characteristics.append(t: cInfo); |
271 | } |
272 | |
273 | QTimer::singleShot(interval: 0, receiver: this, slot: &Device::characteristicsUpdated); |
274 | } |
275 | |
276 | void Device::deviceConnected() |
277 | { |
278 | setUpdate("Back\n(Discovering services...)" ); |
279 | connected = true; |
280 | //! [les-service-2] |
281 | controller->discoverServices(); |
282 | //! [les-service-2] |
283 | } |
284 | |
285 | void Device::errorReceived(QLowEnergyController::Error /*error*/) |
286 | { |
287 | qWarning() << "Error: " << controller->errorString(); |
288 | setUpdate(QString("Back\n(%1)" ).arg(a: controller->errorString())); |
289 | } |
290 | |
291 | void Device::setUpdate(const QString &message) |
292 | { |
293 | m_message = message; |
294 | emit updateChanged(); |
295 | } |
296 | |
297 | void Device::disconnectFromDevice() |
298 | { |
299 | // UI always expects disconnect() signal when calling this signal |
300 | // TODO what is really needed is to extend state() to a multi value |
301 | // and thus allowing UI to keep track of controller progress in addition to |
302 | // device scan progress |
303 | |
304 | if (controller->state() != QLowEnergyController::UnconnectedState) |
305 | controller->disconnectFromDevice(); |
306 | else |
307 | deviceDisconnected(); |
308 | } |
309 | |
310 | void Device::deviceDisconnected() |
311 | { |
312 | qWarning() << "Disconnect from device" ; |
313 | emit disconnected(); |
314 | } |
315 | |
316 | void Device::serviceDetailsDiscovered(QLowEnergyService::ServiceState newState) |
317 | { |
318 | if (newState != QLowEnergyService::ServiceDiscovered) { |
319 | // do not hang in "Scanning for characteristics" mode forever |
320 | // in case the service discovery failed |
321 | // We have to queue the signal up to give UI time to even enter |
322 | // the above mode |
323 | if (newState != QLowEnergyService::DiscoveringServices) { |
324 | QMetaObject::invokeMethod(obj: this, member: "characteristicsUpdated" , |
325 | type: Qt::QueuedConnection); |
326 | } |
327 | return; |
328 | } |
329 | |
330 | auto service = qobject_cast<QLowEnergyService *>(object: sender()); |
331 | if (!service) |
332 | return; |
333 | |
334 | |
335 | |
336 | //! [les-chars] |
337 | const QList<QLowEnergyCharacteristic> chars = service->characteristics(); |
338 | for (const QLowEnergyCharacteristic &ch : chars) { |
339 | auto cInfo = new CharacteristicInfo(ch); |
340 | m_characteristics.append(t: cInfo); |
341 | } |
342 | //! [les-chars] |
343 | |
344 | emit characteristicsUpdated(); |
345 | } |
346 | |
347 | void Device::deviceScanError(QBluetoothDeviceDiscoveryAgent::Error error) |
348 | { |
349 | if (error == QBluetoothDeviceDiscoveryAgent::PoweredOffError) |
350 | setUpdate("The Bluetooth adaptor is powered off, power it on before doing discovery." ); |
351 | else if (error == QBluetoothDeviceDiscoveryAgent::InputOutputError) |
352 | setUpdate("Writing or reading from the device resulted in an error." ); |
353 | else { |
354 | static QMetaEnum qme = discoveryAgent->metaObject()->enumerator( |
355 | index: discoveryAgent->metaObject()->indexOfEnumerator(name: "Error" )); |
356 | setUpdate("Error: " + QLatin1String(qme.valueToKey(value: error))); |
357 | } |
358 | |
359 | m_deviceScanState = false; |
360 | emit devicesUpdated(); |
361 | emit stateChanged(); |
362 | } |
363 | |
364 | bool Device::state() |
365 | { |
366 | return m_deviceScanState; |
367 | } |
368 | |
369 | bool Device::hasControllerError() const |
370 | { |
371 | return (controller && controller->error() != QLowEnergyController::NoError); |
372 | } |
373 | |
374 | bool Device::isRandomAddress() const |
375 | { |
376 | return randomAddress; |
377 | } |
378 | |
379 | void Device::setRandomAddress(bool newValue) |
380 | { |
381 | randomAddress = newValue; |
382 | emit randomAddressChanged(); |
383 | } |
384 | |