mirror of
https://github.com/kidfromjupiter/nearby.git
synced 2026-09-16 15:36:12 -04:00
[BLE Refactor] Branches from googlemac/iPhone/Shared/Nearby/Sockets and Ble/GNCMBleConnection.* to re-construct ios connection socket.
PiperOrigin-RevId: 455558621
This commit is contained in:
committed by
Copybara-Service
parent
6bd1a78eeb
commit
ecbf356fbd
@@ -21,7 +21,9 @@ objc_library(
|
||||
name = "Mediums",
|
||||
srcs = [
|
||||
"Ble/GNCMBleCentral.m",
|
||||
"Ble/GNCMBleConnection.m",
|
||||
"Ble/GNCMBlePeripheral.m",
|
||||
"Ble/GNCMBleUtils.m",
|
||||
"GNCLeaks.m",
|
||||
"GNCMConnection.m",
|
||||
"WifiLan/GNCMBonjourBrowser.m",
|
||||
@@ -31,7 +33,9 @@ objc_library(
|
||||
],
|
||||
hdrs = [
|
||||
"Ble/GNCMBleCentral.h",
|
||||
"Ble/GNCMBleConnection.h",
|
||||
"Ble/GNCMBlePeripheral.h",
|
||||
"Ble/GNCMBleUtils.h",
|
||||
"GNCLeaks.h",
|
||||
"GNCMConnection.h",
|
||||
"WifiLan/GNCMBonjourBrowser.h",
|
||||
@@ -42,6 +46,9 @@ objc_library(
|
||||
deps = [
|
||||
":ObjCProtos",
|
||||
"//internal/platform/implementation/ios:Shared",
|
||||
"//internal/platform/implementation/ios/Mediums/Ble/Sockets:Central",
|
||||
"//internal/platform/implementation/ios/Mediums/Ble/Sockets:Peripheral",
|
||||
"//internal/platform/implementation/ios/Mediums/Ble/Sockets:Shared",
|
||||
"//third_party/objective_c/google_toolbox_for_mac:GTM_Logger",
|
||||
"@com_google_absl//absl/numeric:int128",
|
||||
],
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright 2022 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/GNCMConnection.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class GNSSocket;
|
||||
|
||||
/**
|
||||
* A GNCMConnection implementation for BLE.
|
||||
* This class is thread-safe.
|
||||
*/
|
||||
@interface GNCMBleConnection : NSObject <GNCMConnection>
|
||||
@property(nonatomic) GNCMConnectionHandlers *connectionHandlers;
|
||||
|
||||
+ (instancetype)connectionWithSocket:(GNSSocket *)socket
|
||||
serviceId:(NSString *)serviceId
|
||||
callbackQueue:(dispatch_queue_t)callbackQueue;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,100 @@
|
||||
// Copyright 2022 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/GNCMBleConnection.h"
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/GNCMBleUtils.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/GNCLeaks.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/GNCMConnection.h"
|
||||
#import "GoogleToolboxForMac/GTMLogger.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface GNCMBleConnection () <GNSSocketDelegate>
|
||||
@property(nonatomic) dispatch_queue_t selfQueue;
|
||||
@property(nonatomic) GNSSocket *socket;
|
||||
@property(nonatomic) NSData *serviceIdHash;
|
||||
@property(nonatomic) dispatch_queue_t callbackQueue;
|
||||
@end
|
||||
|
||||
@implementation GNCMBleConnection
|
||||
|
||||
+ (instancetype)connectionWithSocket:(GNSSocket *)socket
|
||||
serviceId:(NSString *)serviceId
|
||||
callbackQueue:(dispatch_queue_t)callbackQueue {
|
||||
GNCMBleConnection *connection = [[GNCMBleConnection alloc] init];
|
||||
connection.socket = socket;
|
||||
socket.delegate = connection;
|
||||
connection.serviceIdHash = GNCMServiceIdHash(serviceId);
|
||||
connection.callbackQueue = callbackQueue;
|
||||
connection.selfQueue = dispatch_queue_create("GNCMBleConnectionQueue", DISPATCH_QUEUE_SERIAL);
|
||||
return connection;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[_socket disconnect];
|
||||
GNCVerifyDealloc(_socket, 2); // assert if not deallocated
|
||||
}
|
||||
|
||||
#pragma mark GNCMConnection
|
||||
|
||||
- (void)sendData:(NSData *)data
|
||||
progressHandler:(GNCMProgressHandler)progressHandler
|
||||
completion:(GNCMPayloadResultHandler)completion {
|
||||
dispatch_async(_selfQueue, ^{
|
||||
[_socket sendData:data
|
||||
progressHandler:^(float progress) {
|
||||
// Convert normalized progress value to number of bytes.
|
||||
dispatch_async(_callbackQueue, ^{
|
||||
progressHandler((size_t)(progress * data.length));
|
||||
});
|
||||
}
|
||||
completion:^(NSError *error) {
|
||||
dispatch_async(_callbackQueue, ^{
|
||||
completion(error ? GNCMPayloadResultFailure : GNCMPayloadResultSuccess);
|
||||
});
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark GNSSocketDelegate
|
||||
|
||||
- (void)socketDidConnect:(GNSSocket *)socket {
|
||||
GTMLoggerError(@"Unexpected -socketDidConnect: call; should've already happened");
|
||||
}
|
||||
|
||||
- (void)socket:(GNSSocket *)socket didDisconnectWithError:(NSError *)error {
|
||||
dispatch_async(_selfQueue, ^{
|
||||
if (_connectionHandlers.disconnectedHandler) {
|
||||
dispatch_async(_callbackQueue, ^{
|
||||
_connectionHandlers.disconnectedHandler();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)socket:(GNSSocket *)socket didReceiveData:(NSData *)data {
|
||||
dispatch_async(_selfQueue, ^{
|
||||
if (_connectionHandlers.payloadHandler) {
|
||||
dispatch_async(_callbackQueue, ^{
|
||||
_connectionHandlers.payloadHandler(data);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright 2022 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import <CoreBluetooth/CoreBluetooth.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class GNSSocket;
|
||||
|
||||
typedef void (^GNCMBoolHandler)(BOOL flag);
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Required lengths of certain BLE advertisement fields. */
|
||||
typedef NS_ENUM(NSUInteger, GNCMBleAdvertisementLength) {
|
||||
/** Length of service ID hash data object, used in the BLE advertisement and the packet prefix. */
|
||||
GNCMBleAdvertisementLengthServiceIdHash = 3,
|
||||
};
|
||||
|
||||
/** Computes a hash from a service ID string. It is used in the NC BLE advertisement. */
|
||||
NSData *GNCMServiceIdHash(NSString *serviceId);
|
||||
|
||||
/**
|
||||
* Calls the completion handler with (a) YES if the GNSSocket connected, or (b) NO if it failed to
|
||||
* connect for any reason. The completion handler is called on the main queue.
|
||||
*/
|
||||
void GNCMWaitForConnection(GNSSocket *socket, GNCMBoolHandler completion);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright 2022 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/GNCMBleUtils.h"
|
||||
|
||||
#import "internal/platform/implementation/ios/GNCUtils.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket.h"
|
||||
#import "GoogleToolboxForMac/GTMLogger.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
static const NSTimeInterval kBleSocketConnectionTimeout = 5.0;
|
||||
|
||||
NSData *GNCMServiceIdHash(NSString *serviceId) {
|
||||
return [GNCSha256String(serviceId)
|
||||
subdataWithRange:NSMakeRange(0, GNCMBleAdvertisementLengthServiceIdHash)];
|
||||
}
|
||||
|
||||
@interface GNCMBleSocketDelegate : NSObject <GNSSocketDelegate>
|
||||
@property(nonatomic) GNCMBoolHandler connectedHandler;
|
||||
@end
|
||||
|
||||
@implementation GNCMBleSocketDelegate
|
||||
|
||||
+ (instancetype)delegateWithConnectedHandler:(GNCMBoolHandler)connectedHandler {
|
||||
GNCMBleSocketDelegate *connection = [[GNCMBleSocketDelegate alloc] init];
|
||||
connection.connectedHandler = connectedHandler;
|
||||
return connection;
|
||||
}
|
||||
|
||||
#pragma mark - GNSSocketDelegate
|
||||
|
||||
- (void)socketDidConnect:(GNSSocket *)socket {
|
||||
_connectedHandler(YES);
|
||||
}
|
||||
|
||||
- (void)socket:(GNSSocket *)socket didDisconnectWithError:(NSError *)error {
|
||||
_connectedHandler(NO);
|
||||
}
|
||||
|
||||
- (void)socket:(GNSSocket *)socket didReceiveData:(NSData *)data {
|
||||
GTMLoggerError(@"Unexpected -didReceiveData: call");
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
void GNCMWaitForConnection(GNSSocket *socket, GNCMBoolHandler completion) {
|
||||
// This function passes YES to the completion when the socket has successfully connected, and
|
||||
// otherwise passes NO to the completion after a timeout of several seconds. We shouldn't retain
|
||||
// the completion after it's been called, so store it in a __block variable and nil it out once
|
||||
// the socket has connected.
|
||||
__block GNCMBoolHandler completionRef = completion;
|
||||
|
||||
// The delegate listens for the socket connection callbacks. It's retained by the block passed to
|
||||
// dispatch_after below, so it will live long enough to do its job.
|
||||
GNCMBleSocketDelegate *delegate =
|
||||
[GNCMBleSocketDelegate delegateWithConnectedHandler:^(BOOL didConnect) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (completionRef) completionRef(didConnect);
|
||||
completionRef = nil;
|
||||
});
|
||||
}];
|
||||
socket.delegate = delegate;
|
||||
dispatch_after(
|
||||
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kBleSocketConnectionTimeout * NSEC_PER_SEC)),
|
||||
dispatch_get_main_queue(), ^{
|
||||
(void)delegate; // make sure it's retained until the timeout
|
||||
if (completionRef) completionRef(NO);
|
||||
});
|
||||
}
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,96 @@
|
||||
load("//tools/build_defs/swift:swift_explicit_module_build_test.bzl", "swift_explicit_module_build_test")
|
||||
|
||||
# Copyright 2020 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
licenses(["notice"])
|
||||
|
||||
package(
|
||||
default_visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "Central",
|
||||
srcs = glob([
|
||||
"Source/Central/*.m",
|
||||
]),
|
||||
hdrs = glob([
|
||||
"Source/Central/*.h",
|
||||
]) + [
|
||||
"Source/GNSCentral.h",
|
||||
],
|
||||
sdk_frameworks = [
|
||||
"CoreBluetooth",
|
||||
"CoreFoundation",
|
||||
"QuartzCore",
|
||||
],
|
||||
deps = [
|
||||
":Shared",
|
||||
"//third_party/apple_frameworks:CoreBluetooth",
|
||||
"//third_party/apple_frameworks:Foundation",
|
||||
"//third_party/objective_c/google_toolbox_for_mac:GTM_Logger",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "Peripheral",
|
||||
srcs = glob([
|
||||
"Source/Peripheral/*.m",
|
||||
]),
|
||||
hdrs = glob([
|
||||
"Source/Peripheral/*.h",
|
||||
]) + [
|
||||
"Source/GNSPeripheral.h",
|
||||
],
|
||||
sdk_frameworks = [
|
||||
"CoreBluetooth",
|
||||
"CoreFoundation",
|
||||
"QuartzCore",
|
||||
],
|
||||
deps = [
|
||||
":Shared",
|
||||
"//third_party/apple_frameworks:CoreBluetooth",
|
||||
"//third_party/apple_frameworks:Foundation",
|
||||
"//third_party/apple_frameworks:UIKit",
|
||||
"//third_party/objective_c/google_toolbox_for_mac:GTM_Logger",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "Shared",
|
||||
srcs = glob([
|
||||
"Source/Shared/*.m",
|
||||
]),
|
||||
hdrs = glob([
|
||||
"Source/Shared/*.h",
|
||||
]) + [
|
||||
"Source/GNSShared.h",
|
||||
],
|
||||
sdk_frameworks = [
|
||||
"CoreBluetooth",
|
||||
"CoreFoundation",
|
||||
"QuartzCore",
|
||||
],
|
||||
deps = [
|
||||
"//third_party/apple_frameworks:CoreBluetooth",
|
||||
"//third_party/apple_frameworks:Foundation",
|
||||
"//third_party/objective_c/google_toolbox_for_mac:GTM_Logger",
|
||||
],
|
||||
)
|
||||
|
||||
swift_explicit_module_build_test(
|
||||
name = "swift_explicit_module_build_test",
|
||||
ignore_headerless_targets = True,
|
||||
minimum_os_version = "8.0",
|
||||
platform_type = "ios",
|
||||
)
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralManager.h"
|
||||
|
||||
#import <CoreBluetooth/CoreBluetooth.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface GNSCentralManager ()<CBCentralManagerDelegate>
|
||||
|
||||
- (void)connectPeripheralForPeer:(GNSCentralPeerManager *)peer
|
||||
options:(nullable NSDictionary *)options;
|
||||
|
||||
- (void)cancelPeripheralConnectionForPeer:(GNSCentralPeerManager *)peer;
|
||||
|
||||
- (void)centralPeerManagerDidDisconnect:(GNSCentralPeerManager *)peer;
|
||||
|
||||
@end
|
||||
|
||||
@interface GNSCentralManager (VisibleForTesting)
|
||||
|
||||
- (CBCentralManager *)testing_cbCentralManager;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import <CoreBluetooth/CoreBluetooth.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class CBUUID;
|
||||
@class GNSCentralManager;
|
||||
@class GNSCentralPeerManager;
|
||||
|
||||
@protocol GNSCentralManagerDelegate<NSObject>
|
||||
|
||||
/**
|
||||
* Called while scanning when a new peer is found.
|
||||
*
|
||||
* @param centralManager Central manager
|
||||
* @param centralPeerManager New central peer.
|
||||
* @param advertisementData The advertisement data received from the peer.
|
||||
*/
|
||||
- (void)centralManager:(GNSCentralManager *)centralManager
|
||||
didDiscoverPeer:(GNSCentralPeerManager *)centralPeerManager
|
||||
advertisementData:(nullable NSDictionary *)advertisementData;
|
||||
|
||||
/**
|
||||
* Called when the Bluetooth Low Energy state is updated. Note that scanning is paused if Bluetooth
|
||||
* is disabled while scanning is on. Scanning will be resumed automatically as soon as Bluetooth is
|
||||
* re-enabled.
|
||||
*
|
||||
* @param centralManager Central manager
|
||||
*/
|
||||
- (void)centralManagerDidUpdateBleState:(GNSCentralManager *)centralManager;
|
||||
|
||||
@end
|
||||
|
||||
/**
|
||||
* GNSCentralManager can search for new GNSCentralPeerManager or can retrieve known
|
||||
* GNSCentralPeerManager. Note that GNSCentralPeerManager retains its GNSCentralManager. Therefore
|
||||
* it is not necessary to keep a strong pointer to GNSCentralManager.
|
||||
*
|
||||
* * To create a GNSCentralManager instance:
|
||||
* GNSCentralManager *centralManager = [[GNSCentralManager alloc] initWithSocketServiceUUID:UUID];
|
||||
* centralManager.delegate = myManagerDelegate;
|
||||
*
|
||||
* * Get a peer:
|
||||
* - To retrieve a known peer:
|
||||
* With a known peer: -[GNSCentralManager retrieveCentralPeerWithIdentifier:]
|
||||
* To retrieve a previous peer, can be called instead of scanning.
|
||||
*
|
||||
* - To scan for peers:
|
||||
* -[GNSCentralManager startScanWithAdvertisedName:advertisedServiceUUID:]
|
||||
* When a peer has been discovered, myManagerDelegate is called:
|
||||
* -[myManagerDelegate centralManager:discoveredPeer:];
|
||||
*
|
||||
* * To create a socket to the peer:
|
||||
* With the received peer, -[GNSCentralPeerManager socketWithPairingCharacteristic:completion:]
|
||||
* has to be called:
|
||||
* [centralPeer socketWithPairingCharacteristic:shouldAddPairingCharacteristics
|
||||
* completion:^(GNSSocket *mySocket, NSError *error) {
|
||||
* if (error) {
|
||||
* NSLog(@"Error to get the socket %@", error);
|
||||
* return;
|
||||
* }
|
||||
* mySocket.delegate = mySocketDelegate;
|
||||
* }];
|
||||
*
|
||||
* To use the socket, please see GNSSocket.
|
||||
*
|
||||
* Once the socket is disconnected, the peer will automatically be disconnected from BLE. Also, if
|
||||
* GNSCentralPeerManager instance is deallocated, the socket will be automatically closed.
|
||||
*
|
||||
* This class is not thread-safe.
|
||||
*/
|
||||
@interface GNSCentralManager : NSObject
|
||||
|
||||
/**
|
||||
* Service used for the socket.
|
||||
*/
|
||||
@property(nonatomic, readonly) CBUUID *socketServiceUUID;
|
||||
|
||||
/**
|
||||
* YES, if scanning is enabled (even when the bluetooth is disabled).
|
||||
*/
|
||||
@property(nonatomic, readonly, getter=isScanning) BOOL scanning;
|
||||
@property(nonatomic, readwrite, weak) id<GNSCentralManagerDelegate> delegate;
|
||||
|
||||
/**
|
||||
* BLE state based on -[CBCentralManager state].
|
||||
*/
|
||||
@property(nonatomic, readonly) CBCentralManagerState cbCentralManagerState;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
/**
|
||||
* Creates an instance of GNSCentralManager. Only one service is supported. This service will be
|
||||
* passed to the GNSCentralPeerManager instances and GNSSocket.
|
||||
*
|
||||
* @param socketServiceUUID UUID used to create peer sockets with all the peers found.
|
||||
* @param queue The queue this object is called on and callbacks are made on.
|
||||
*
|
||||
* @return GNSCentralManager instance
|
||||
*/
|
||||
- (nullable instancetype)initWithSocketServiceUUID:(CBUUID *)socketServiceUUID
|
||||
queue:(dispatch_queue_t)queue
|
||||
NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
/**
|
||||
* Creates an instance of GNSCentralManager using the main queue for callbacks.
|
||||
*/
|
||||
- (nullable instancetype)initWithSocketServiceUUID:(CBUUID *)socketServiceUUID;
|
||||
|
||||
/**
|
||||
* TODO(sacomoto): Cleanup and merge this and the method below.
|
||||
*
|
||||
* Starts scanning for all Bluetooth Low Energy peers that advertise the service
|
||||
* |advertisedServiceUUID| or the name |advertisedName|.
|
||||
*
|
||||
* If |advertisedName| is non-nil, this central manager will scan for all peripherals and filter
|
||||
* peripherals that advertise |advertisedServiceUUID| or |advertisedName|.
|
||||
*
|
||||
* If |advertisedServiceUUID| is non-nil, the central manager will scan for all peripherals that
|
||||
* advertise |self.socketServiceUUID|.
|
||||
*
|
||||
* If both |advertisedName| and |advertisedServiceUUID| are nil, the central manager will scan for
|
||||
* all peripherals without any filtering.
|
||||
*
|
||||
* Note that |self.socketServiceUUID| and |advertisedServiceUUID| may be different. In this case,
|
||||
* the manager will scan for devices advertising |advertisedServiceUUID| but will use
|
||||
* |self.socketServiceUUID| to transfer data.
|
||||
*
|
||||
* The delegate will be notified when each peer is found.
|
||||
*
|
||||
* @param advertisedName The name advertised by peripheral.
|
||||
* @param advertisedServiceUUID The service advertised by the peripheral.
|
||||
*/
|
||||
- (void)startScanWithAdvertisedName:(nullable NSString *)advertisedName
|
||||
advertisedServiceUUID:(nullable CBUUID *)advertisedServiceUUID;
|
||||
|
||||
/**
|
||||
* Starts scanning for all Bluetooth Low Energy peers that advertise some service in
|
||||
* |advertisedServiceUUIDs|. The delegate will be notified when each peer is found.
|
||||
*
|
||||
* @param advertisedServiceUUID The service advertised by the peripheral.
|
||||
*/
|
||||
- (void)startScanWithAdvertisedServiceUUIDs:(nullable NSArray<CBUUID *> *)advertisedServiceUUIDs;
|
||||
|
||||
/**
|
||||
* Stops scanning.
|
||||
*/
|
||||
- (void)stopScan;
|
||||
|
||||
/**
|
||||
* Starts "no-scan" mode, where identifiers of peripherals already discovered can be be passed into
|
||||
* |-retrievePeripheralWithIdentifier:advertisementData:| for use as socket peripherals. If valid,
|
||||
* the delegate will be notified. The peripheral must be advertising a service in
|
||||
* |advertisedServiceUUIDs|.
|
||||
*
|
||||
* @param advertisedServiceUUID The service(s) the peripherals must be advertising.
|
||||
*/
|
||||
- (void)startNoScanModeWithAdvertisedServiceUUIDs:
|
||||
(nullable NSArray<CBUUID *> *)advertisedServiceUUIDs;
|
||||
|
||||
/**
|
||||
* Tries to retrieve a peripheral that matches the given identifier, and, if successful, treats it
|
||||
* as a newly found peripheral.
|
||||
*
|
||||
* @param identifier The peripheral identifier.
|
||||
* @param advertisementData The advertisement data associated with the peripheral.
|
||||
*/
|
||||
- (void)retrievePeripheralWithIdentifier:(NSUUID *)identifier
|
||||
advertisementData:(NSDictionary<NSString *, id> *)advertisementData;
|
||||
|
||||
/**
|
||||
* Stops "no-scan" mode.
|
||||
*/
|
||||
- (void)stopNoScanMode;
|
||||
|
||||
/**
|
||||
* Creates a GNSCentralPeerManager for an identifier.
|
||||
*
|
||||
* @param identifier Identifier from the idenfier.
|
||||
*
|
||||
* @return Central peer.
|
||||
*/
|
||||
- (nullable GNSCentralPeerManager *)retrieveCentralPeerWithIdentifier:(NSUUID *)identifier;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
+372
@@ -0,0 +1,372 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralManager.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralManager+Private.h"
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralPeerManager+Private.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralPeerManager.h"
|
||||
#import "GoogleToolboxForMac/GTMLogger.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
static NSString *CentralManagerStateString(CBCentralManagerState state) {
|
||||
switch (state) {
|
||||
case CBCentralManagerStateUnknown:
|
||||
return @"CBCentralManagerStateUnknown";
|
||||
case CBCentralManagerStateResetting:
|
||||
return @"CBCentralManagerStateResetting";
|
||||
case CBCentralManagerStateUnsupported:
|
||||
return @"CBCentralManagerStateUnsupported";
|
||||
case CBCentralManagerStateUnauthorized:
|
||||
return @"CBCentralManagerStateUnauthorized";
|
||||
case CBCentralManagerStatePoweredOff:
|
||||
return @"CBCentralManagerStatePoweredOff";
|
||||
case CBCentralManagerStatePoweredOn:
|
||||
return @"CBCentralManagerStatePoweredOn";
|
||||
}
|
||||
return [NSString stringWithFormat:@"CBCentralManagerState Unknown(%ld)", (long)state];
|
||||
}
|
||||
|
||||
@interface GNSCentralManager () {
|
||||
NSString *_advertisedName;
|
||||
CBUUID *_advertisedServiceUUID;
|
||||
NSArray<CBUUID *> *_advertisedServiceUUIDs;
|
||||
CBCentralManager *_cbCentralManager;
|
||||
NSMapTable *_centralPeerManagers;
|
||||
BOOL _cbCentralScanStarted;
|
||||
dispatch_queue_t _queue;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation GNSCentralManager
|
||||
|
||||
+ (CBCentralManager *)centralManagerWithDelegate:(id<CBCentralManagerDelegate>)delegate
|
||||
queue:(nullable dispatch_queue_t)queue
|
||||
options:(nullable NSDictionary<NSString *, id> *)options {
|
||||
return [[CBCentralManager alloc] initWithDelegate:delegate queue:queue options:options];
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
[self doesNotRecognizeSelector:_cmd];
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (nullable instancetype)initWithSocketServiceUUID:(CBUUID *)socketServiceUUID
|
||||
queue:(dispatch_queue_t)queue {
|
||||
NSAssert(socketServiceUUID, @"Cannot create a GNSCentralManager with nil service UUID.");
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_socketServiceUUID = socketServiceUUID;
|
||||
_queue = queue;
|
||||
_cbCentralManager = [[self class]
|
||||
centralManagerWithDelegate:self
|
||||
queue:queue
|
||||
options:@{CBCentralManagerOptionShowPowerAlertKey : @NO}];
|
||||
_centralPeerManagers = [NSMapTable strongToWeakObjectsMapTable];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (nullable instancetype)initWithSocketServiceUUID:(CBUUID *)socketServiceUUID {
|
||||
return [self initWithSocketServiceUUID:socketServiceUUID queue:dispatch_get_main_queue()];
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
_cbCentralManager.delegate = nil;
|
||||
}
|
||||
|
||||
- (void)startScanWithAdvertisedName:(nullable NSString *)advertisedName
|
||||
advertisedServiceUUID:(nullable CBUUID *)advertisedServiceUUID {
|
||||
NSAssert(_cbCentralManager, @"CBCentralManager not created.");
|
||||
if (_scanning) {
|
||||
return;
|
||||
}
|
||||
_advertisedServiceUUID = advertisedServiceUUID;
|
||||
_advertisedName = advertisedName;
|
||||
_scanning = YES;
|
||||
if (_cbCentralManager.state == CBCentralManagerStatePoweredOn) {
|
||||
[self startCBScan];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)startScanWithAdvertisedServiceUUIDs:(nullable NSArray<CBUUID *> *)advertisedServiceUUIDs {
|
||||
NSAssert(_cbCentralManager, @"CBCentralManager not created.");
|
||||
if (_scanning) {
|
||||
return;
|
||||
}
|
||||
[self startNoScanModeWithAdvertisedServiceUUIDs:_advertisedServiceUUIDs];
|
||||
_scanning = YES;
|
||||
if (_cbCentralManager.state == CBCentralManagerStatePoweredOn) {
|
||||
[self startCBScan];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)stopScan {
|
||||
if (!_scanning) {
|
||||
return;
|
||||
}
|
||||
_advertisedName = nil;
|
||||
if (_cbCentralScanStarted) {
|
||||
[self stopCBScan];
|
||||
}
|
||||
_scanning = NO;
|
||||
[self stopNoScanMode];
|
||||
}
|
||||
|
||||
- (void)startNoScanModeWithAdvertisedServiceUUIDs:
|
||||
(nullable NSArray<CBUUID *> *)advertisedServiceUUIDs {
|
||||
_advertisedServiceUUIDs = [advertisedServiceUUIDs copy];
|
||||
}
|
||||
|
||||
- (void)retrievePeripheralWithIdentifier:(NSUUID *)identifier
|
||||
advertisementData:(nonnull NSDictionary<NSString *, id> *)advertisementData {
|
||||
NSArray<CBPeripheral *> *peripherals =
|
||||
[_cbCentralManager retrievePeripheralsWithIdentifiers:@[identifier]];
|
||||
if (peripherals.count > 0) {
|
||||
[self centralManager:_cbCentralManager
|
||||
didDiscoverPeripheral:peripherals[0]
|
||||
advertisementData:advertisementData
|
||||
RSSI:@(127)]; // RSSI not available
|
||||
}
|
||||
}
|
||||
|
||||
- (void)stopNoScanMode {
|
||||
_advertisedServiceUUIDs = nil;
|
||||
}
|
||||
|
||||
- (nullable GNSCentralPeerManager *)retrieveCentralPeerWithIdentifier:(NSUUID *)identifier {
|
||||
NSAssert(_cbCentralManager, @"CBCentralManager not created.");
|
||||
NSAssert(identifier, @"Should have an identifier, self: %@", self);
|
||||
GNSCentralPeerManager *peerManager = [_centralPeerManagers objectForKey:identifier];
|
||||
if (peerManager) {
|
||||
GTMLoggerAssert(@"Previous GNSCentralPeerManager still alive, self: %@, peer manager: %@", self,
|
||||
peerManager);
|
||||
return nil;
|
||||
}
|
||||
NSArray<CBPeripheral *> *peripherals =
|
||||
[_cbCentralManager retrievePeripheralsWithIdentifiers:@[ identifier ]];
|
||||
for (CBPeripheral *peripheral in peripherals) {
|
||||
if ([peripheral.identifier isEqual:identifier]) {
|
||||
peerManager = [self createCentralPeerManagerWithPeripheral:peripheral];
|
||||
[_centralPeerManagers setObject:peerManager forKey:identifier];
|
||||
return peerManager;
|
||||
}
|
||||
}
|
||||
GTMLoggerError(@"CBPeripheral not found, self: %@, identifier %@, peripherals %@", self,
|
||||
identifier, peripherals);
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (CBCentralManagerState)cbCentralManagerState {
|
||||
// Cast to avoid some warnings in XCode 8. When Xcode 8 is the default
|
||||
// swap CBCentralManagerState for CBManagerState.
|
||||
return (CBCentralManagerState)_cbCentralManager.state;
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
return [NSString stringWithFormat:@"<%@: %p, socket service UUID: %@, advertised name: %@, "
|
||||
@"scanning: %@, central scan started: %@, bluetooth state %@, "
|
||||
@"CBCentralManager %@, peer managers: %@>",
|
||||
NSStringFromClass([self class]), self, _socketServiceUUID,
|
||||
_advertisedName, _scanning ? @"YES" : @"NO",
|
||||
_cbCentralScanStarted ? @"YES" : @"NO",
|
||||
CentralManagerStateString([self cbCentralManagerState]),
|
||||
_cbCentralManager, _centralPeerManagers];
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (void)startCBScan {
|
||||
if (_cbCentralScanStarted) {
|
||||
return;
|
||||
}
|
||||
_cbCentralScanStarted = YES;
|
||||
// CBCentralManagerScanOptionAllowDuplicatesKey has to be set to YES. There are some cases where
|
||||
// CoreBluetooth never discovers the phone if the setup mode on the phone is enable after opening
|
||||
// the setup window on OS X.
|
||||
// The drawback is the same CBPeripheral is discovered several times per second.
|
||||
NSDictionary<NSString *, id> *options = @{CBCentralManagerScanOptionAllowDuplicatesKey : @YES};
|
||||
if (_advertisedName) {
|
||||
GTMLoggerInfo(@"Start scanning for all peripherals. Filter peripherals with advertised "
|
||||
"service UUID %@ or advertised name %@.",
|
||||
[_socketServiceUUID UUIDString], _advertisedName);
|
||||
[_cbCentralManager scanForPeripheralsWithServices:nil options:options];
|
||||
} else if (_advertisedServiceUUID) {
|
||||
// Not logging this case to avoid spamming the logs.
|
||||
[_cbCentralManager scanForPeripheralsWithServices:@[ _advertisedServiceUUID ] options:options];
|
||||
} else if (_advertisedServiceUUIDs) {
|
||||
// Not logging this case to avoid spamming the logs.
|
||||
[_cbCentralManager scanForPeripheralsWithServices:_advertisedServiceUUIDs options:options];
|
||||
} else {
|
||||
GTMLoggerInfo(@"Start scanning for all peripherals.");
|
||||
[_cbCentralManager scanForPeripheralsWithServices:nil options:options];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)stopCBScan {
|
||||
if (!_cbCentralScanStarted) {
|
||||
return;
|
||||
}
|
||||
_cbCentralScanStarted = NO;
|
||||
[_cbCentralManager stopScan];
|
||||
}
|
||||
|
||||
- (void)peripheralDisconnected:(CBPeripheral *)peripheral withError:(NSError *)error {
|
||||
NSAssert(peripheral.state == CBPeripheralStateDisconnected,
|
||||
@"Peripheral should be disconnected %@", peripheral);
|
||||
GNSCentralPeerManager *peerManager = [self centralPeerForPeripheral:peripheral];
|
||||
if (peerManager) {
|
||||
[peerManager bleDisconnectedWithError:error];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)connectPeripheralForPeer:(GNSCentralPeerManager *)peer
|
||||
options:(nullable NSDictionary<NSString *, id> *)options {
|
||||
GTMLoggerInfo(@"Connect peer %@ options %@", peer, options);
|
||||
[_cbCentralManager connectPeripheral:peer.cbPeripheral options:options];
|
||||
}
|
||||
|
||||
- (void)cancelPeripheralConnectionForPeer:(GNSCentralPeerManager *)peer {
|
||||
GTMLoggerInfo(@"Cancel peer connection %@", peer);
|
||||
[_cbCentralManager cancelPeripheralConnection:peer.cbPeripheral];
|
||||
}
|
||||
|
||||
- (void)centralPeerManagerDidDisconnect:(GNSCentralPeerManager *)peer {
|
||||
GTMLoggerInfo(@"Central manager removing central peer manager, central manager: %@, peer %@",
|
||||
self, peer);
|
||||
if (peer.cbPeripheral.state != CBPeripheralStateDisconnected) {
|
||||
GTMLoggerInfo(@"Unexpected peripheral state %@", peer.cbPeripheral);
|
||||
}
|
||||
}
|
||||
|
||||
- (CBCentralManager *)cbCentralManager {
|
||||
return _cbCentralManager;
|
||||
}
|
||||
|
||||
- (GNSCentralPeerManager *)createCentralPeerManagerWithPeripheral:(CBPeripheral *)peripheral {
|
||||
return [[GNSCentralPeerManager alloc]
|
||||
initWithPeripheral:peripheral centralManager:self queue:_queue];
|
||||
}
|
||||
|
||||
- (GNSCentralPeerManager *)centralPeerForPeripheral:(CBPeripheral *)peripheral {
|
||||
NSParameterAssert(peripheral);
|
||||
GNSCentralPeerManager *peerManager = [_centralPeerManagers objectForKey:peripheral.identifier];
|
||||
if (!peerManager) {
|
||||
GTMLoggerDebug(@"No peer manager found for peripheral %@", peripheral);
|
||||
return nil;
|
||||
}
|
||||
if (peerManager.cbPeripheral != peripheral) {
|
||||
// There is a peer manager with a different peripheral object than |peripheral|, but with the
|
||||
// same identifier. Something really wrong happened in CoreBluetooth here.
|
||||
GTMLoggerError(@"Peer Manager %@ has a different peripheral than %@ [CentralManager = %@]",
|
||||
peerManager, peripheral, self);
|
||||
return nil;
|
||||
}
|
||||
return peerManager;
|
||||
}
|
||||
|
||||
#pragma mark - CBCentralManagerDelegate
|
||||
|
||||
- (void)centralManagerDidUpdateState:(CBCentralManager *)central {
|
||||
NSAssert(central == _cbCentralManager, @"Wrong peripheral manager.");
|
||||
GTMLoggerInfo(@"CoreBluetooth state: %@",
|
||||
CentralManagerStateString([self cbCentralManagerState]));
|
||||
switch (central.state) {
|
||||
case CBCentralManagerStatePoweredOn: {
|
||||
if (_scanning) {
|
||||
[self startCBScan];
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CBCentralManagerStatePoweredOff:
|
||||
case CBCentralManagerStateResetting:
|
||||
case CBCentralManagerStateUnauthorized:
|
||||
case CBCentralManagerStateUnsupported:
|
||||
case CBCentralManagerStateUnknown:
|
||||
[self stopCBScan];
|
||||
break;
|
||||
}
|
||||
[_delegate centralManagerDidUpdateBleState:self];
|
||||
for (GNSCentralPeerManager *peerManager in _centralPeerManagers.objectEnumerator) {
|
||||
[peerManager cbCentralManagerStateDidUpdate];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)centralManager:(CBCentralManager *)central
|
||||
didDiscoverPeripheral:(CBPeripheral *)peripheral
|
||||
advertisementData:(NSDictionary<NSString *, id> *)advertisementData
|
||||
RSSI:(NSNumber *)RSSI {
|
||||
// This method can be called several times per second since
|
||||
// CBCentralManagerScanOptionAllowDuplicatesKey is set to YES. Therefore, don't log.
|
||||
NSArray<CBUUID *> *advertisedServiceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey];
|
||||
NSString *advertisedName = advertisementData[CBAdvertisementDataLocalNameKey];
|
||||
BOOL noAdvertisementFilter = !_advertisedServiceUUID && !_advertisedName;
|
||||
if (noAdvertisementFilter || [advertisedServiceUUIDs containsObject:_advertisedServiceUUID] ||
|
||||
[_advertisedName isEqualToString:advertisedName]) {
|
||||
NSUUID *identifier = peripheral.identifier;
|
||||
GNSCentralPeerManager *peerManager = [_centralPeerManagers objectForKey:identifier];
|
||||
if (!peerManager) {
|
||||
GTMLoggerInfo(@"Discovered peer peripheral %@", peripheral);
|
||||
peerManager = [self createCentralPeerManagerWithPeripheral:peripheral];
|
||||
[_centralPeerManagers setObject:peerManager forKey:identifier];
|
||||
[_delegate centralManager:self
|
||||
didDiscoverPeer:peerManager
|
||||
advertisementData:advertisementData];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral {
|
||||
NSAssert(central == _cbCentralManager, @"Unexpected central manager");
|
||||
GTMLoggerInfo(@"Connected to %@", peripheral);
|
||||
GNSCentralPeerManager *peerManager = [self centralPeerForPeripheral:peripheral];
|
||||
if (!peerManager) {
|
||||
GTMLoggerError(
|
||||
@"No peer manager found for connected peripheral %@. Cancel peripheral connection",
|
||||
peripheral);
|
||||
[_cbCentralManager cancelPeripheralConnection:peripheral];
|
||||
return;
|
||||
}
|
||||
[peerManager bleConnected];
|
||||
}
|
||||
|
||||
- (void)centralManager:(CBCentralManager *)central
|
||||
didFailToConnectPeripheral:(CBPeripheral *)peripheral
|
||||
error:(nullable NSError *)error {
|
||||
NSAssert(central == _cbCentralManager, @"Unexpected central manager");
|
||||
GTMLoggerInfo(@"Fail to connect to %@, error %@", peripheral, error);
|
||||
[self peripheralDisconnected:peripheral withError:error];
|
||||
}
|
||||
|
||||
- (void)centralManager:(CBCentralManager *)central
|
||||
didDisconnectPeripheral:(CBPeripheral *)peripheral
|
||||
error:(nullable NSError *)error {
|
||||
NSAssert(central == _cbCentralManager, @"Unexpected central manager");
|
||||
GTMLoggerInfo(@"Did disconnect %@, error %@", peripheral, error);
|
||||
[self peripheralDisconnected:peripheral withError:error];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation GNSCentralManager (VisibleForTesting)
|
||||
|
||||
- (CBCentralManager *)testing_cbCentralManager {
|
||||
return _cbCentralManager;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralPeerManager.h"
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket+Private.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class CBPeripheral;
|
||||
@class GNSCentralManager;
|
||||
|
||||
@interface GNSCentralPeerManager ()<GNSSocketOwner, CBPeripheralDelegate>
|
||||
|
||||
@property(nonatomic, readonly) CBPeripheral *cbPeripheral;
|
||||
|
||||
/**
|
||||
* Creates a new instance of GNSCentralPeerManager. Should only be used by GNSCentralManager.
|
||||
*
|
||||
* @param peripheral CoreBluetooth peripheral instance
|
||||
* @param centralManager Central manager
|
||||
* @param queue The queue this object is called on and callbacks are made on.
|
||||
*
|
||||
* @return GNSCentralPeerManager instance.
|
||||
*/
|
||||
- (nullable instancetype)initWithPeripheral:(CBPeripheral *)peripheral
|
||||
centralManager:(GNSCentralManager *)centralManager
|
||||
queue:(dispatch_queue_t)queue
|
||||
NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
/**
|
||||
* Creates a new instance of GNSCentralPeerManager using the main queue for callbacks.
|
||||
*/
|
||||
- (nullable instancetype)initWithPeripheral:(CBPeripheral *)peripheral
|
||||
centralManager:(GNSCentralManager *)centralManager;
|
||||
|
||||
/**
|
||||
* Called by the central manager when the peripheral is connected with BLE.
|
||||
*/
|
||||
- (void)bleConnected;
|
||||
|
||||
/**
|
||||
* Called by the central manager when the peripheral is disconnected from BLE.
|
||||
*
|
||||
* @param error Error while being disconnected.
|
||||
*/
|
||||
- (void)bleDisconnectedWithError:(nullable NSError *)error;
|
||||
|
||||
/**
|
||||
* Called by the central manager when the bluetooth state changes.
|
||||
*/
|
||||
- (void)cbCentralManagerStateDidUpdate;
|
||||
|
||||
@end
|
||||
|
||||
@interface GNSCentralPeerManager (TestingHelpers)
|
||||
|
||||
- (NSTimer *)testing_connectionConfirmTimer;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class CBUUID;
|
||||
@class GNSCentralPeerManager;
|
||||
@class GNSSocket;
|
||||
|
||||
typedef void (^GNSCentralSocketCompletion)(GNSSocket *socket, NSError *error);
|
||||
typedef void (^GNSPairingCompletion)(BOOL pairing, NSError *error);
|
||||
typedef void (^GNSReadRRSIValueCompletion)(NSNumber *rssi, NSError *error);
|
||||
|
||||
/**
|
||||
* This class manage one CBPeripheral. It can only manage one service. The service UUID is set
|
||||
* by GNSCentralManager. The GNSCentralPeerManager instance is created by GNSCentralManager.
|
||||
* From this class a socket can be created using -[GNSCentralPeerManager socketWithCompletion:].
|
||||
* See GNSCentralManager header for more information.
|
||||
*
|
||||
* This class is not thread-safe.
|
||||
*/
|
||||
@interface GNSCentralPeerManager : NSObject
|
||||
|
||||
/**
|
||||
* MTU value. The default is 100.
|
||||
*/
|
||||
@property(nonatomic) NSUInteger socketMaximumUpdateValueLength;
|
||||
|
||||
/**
|
||||
* Peer identifier.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSUUID *identifier;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
/**
|
||||
* Creates the socket. When the socket is created and ready to use, |completion| is called. |data|
|
||||
* is sent during the connection handshake, and is going to be treated as the first message on the
|
||||
* peripheral/server, as established by the Weave BLE protocol. |data| can contain at most 13 bytes.
|
||||
*
|
||||
* If the socket fails to be created |completion| is called with an error. This method should be
|
||||
* called once until the completion is called, otherwise |completion| will be called with an
|
||||
* operation in progress error.
|
||||
*
|
||||
* @param handshakeData Data sent during the connection handshake. Should contain at most 13 bytes.
|
||||
* @param pairingCharacteristic If YES, peripheral need to have the pairing characteristic in order
|
||||
* to get the socket. The pairing characteristic is required to trigger the pairing.
|
||||
* @param completion Handler called when the socket is created or failed to be created. It is
|
||||
* guaranteed that either the socket or the error passed to |completion| are not nil.
|
||||
*/
|
||||
- (void)socketWithHandshakeData:(NSData *)handshakeData
|
||||
pairingCharacteristic:(BOOL)hasPairingCharacteristic
|
||||
completion:(GNSCentralSocketCompletion)completion;
|
||||
|
||||
/**
|
||||
* See -[GNSCentralPeerManager socketWithHandshakeData:pairingCharacteristic:completion:].
|
||||
*/
|
||||
- (void)socketWithPairingCharacteristic:(BOOL)shouldAddPairingCharacteristics
|
||||
completion:(GNSCentralSocketCompletion)completion;
|
||||
|
||||
/**
|
||||
* Cancel the pending socket request, if any. |completion| (passed in
|
||||
* |socketWithPairingCharacteristic:completion:|) will be called (with a nil socket) when the
|
||||
* pending socket request is successfully cancelled (and disconnected).
|
||||
*
|
||||
* Note that, after calling |cancelPendingSocket| is still necessary to wait for the |completion|
|
||||
* (passed in a previous |socketWithPairingCharacteristic:completion:| invocation) to be called
|
||||
* before calling |socketWithPairingCharacteristic:completion:| again.
|
||||
*/
|
||||
- (void)cancelPendingSocket;
|
||||
|
||||
/**
|
||||
* Triggers pairing between the central and the peripheral. The socket must have been generated
|
||||
* with pairing characteristic. The completion has to be called before calling this method again.
|
||||
*
|
||||
* @param completion Completion called when the pairing has failed or succeed.
|
||||
*/
|
||||
- (void)startBluetoothPairingWithCompletion:(GNSPairingCompletion)completion;
|
||||
|
||||
/**
|
||||
* Retrieve the RSSI value while the central peer is connected. If a completion is pending while
|
||||
* this method is called again, the second completion will be added.
|
||||
*
|
||||
* @param completion Called when the value is available. Cannot be nil.
|
||||
*/
|
||||
- (void)readRSSIWithCompletion:(GNSReadRRSIValueCompletion)completion;
|
||||
|
||||
@end
|
||||
+766
@@ -0,0 +1,766 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralPeerManager.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralPeerManager+Private.h"
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralManager+Private.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralManager.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket+Private.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSUtils.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSUtils+Private.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSWeavePacket.h"
|
||||
#import "GoogleToolboxForMac/GTMLogger.h"
|
||||
|
||||
static const NSTimeInterval kMaxConnectionConfirmWaitTimeInSeconds = 2;
|
||||
static const NSTimeInterval kPeripheralFailedToConnectTimeout = 5.0;
|
||||
static const NSTimeInterval kChracteristicWriteTimeoutInSeconds = 0.5;
|
||||
|
||||
// The Weave BLE protocol has only one valid version.
|
||||
static const UInt16 kWeaveVersionSupported = 1;
|
||||
|
||||
typedef NS_ENUM(NSInteger, GNSCentralPeerManagerState) {
|
||||
GNSCentralPeerManagerStateNotConnected = 0,
|
||||
GNSCentralPeerManagerStateBleConnecting = 1,
|
||||
GNSCentralPeerManagerStateBleConnected = 2,
|
||||
GNSCentralPeerManagerStateDiscoveringService = 3,
|
||||
GNSCentralPeerManagerStateDiscoveringCharacteristic = 4,
|
||||
GNSCentralPeerManagerStateSettingNotifications = 5,
|
||||
GNSCentralPeerManagerStateSocketCommunication = 6,
|
||||
// "BLEDisconnecting" is optional. If CoreBluetooth notifies that the central is disconnected,
|
||||
// the state will switch from any state to "NotConnected" without going through
|
||||
// "BLEDisconnecting".
|
||||
GNSCentralPeerManagerStateBleDisconnecting = 7,
|
||||
};
|
||||
|
||||
typedef void (^GNSCentralPeerManagerDiscoverCharacteristicsCallback)(NSError *error);
|
||||
|
||||
static NSUInteger gGNSCentralPeerManagerMaximumUpdateValue = 100;
|
||||
|
||||
static NSString *StateDescription(GNSCentralPeerManagerState state) {
|
||||
switch (state) {
|
||||
case GNSCentralPeerManagerStateNotConnected:
|
||||
return @"Not connected";
|
||||
case GNSCentralPeerManagerStateBleConnecting:
|
||||
return @"BLE Connecting";
|
||||
case GNSCentralPeerManagerStateBleConnected:
|
||||
return @"BLE Connected";
|
||||
case GNSCentralPeerManagerStateDiscoveringService:
|
||||
return @"Discovering service";
|
||||
case GNSCentralPeerManagerStateDiscoveringCharacteristic:
|
||||
return @"Discovering characteristics";
|
||||
case GNSCentralPeerManagerStateSettingNotifications:
|
||||
return @"Setting characteristic notification";
|
||||
case GNSCentralPeerManagerStateSocketCommunication:
|
||||
return @"Socket communication";
|
||||
case GNSCentralPeerManagerStateBleDisconnecting:
|
||||
return @"BLE Disconnecting";
|
||||
}
|
||||
return @"Unknown";
|
||||
}
|
||||
|
||||
static NSString *PeripheralStateString(CBPeripheralState state) {
|
||||
switch (state) {
|
||||
case CBPeripheralStateDisconnected:
|
||||
return @"CBPeripheralStateDisconnected";
|
||||
case CBPeripheralStateConnecting:
|
||||
return @"CBPeripheralStateConnecting";
|
||||
case CBPeripheralStateConnected:
|
||||
return @"CBPeripheralStateConnected";
|
||||
case CBPeripheralStateDisconnecting:
|
||||
return @"CBPeripheralStateDisconnecting";
|
||||
}
|
||||
return [NSString stringWithFormat:@"CBPeripheralState Unknown (%ld)", (long)state];
|
||||
}
|
||||
|
||||
@interface GNSCentralPeerManager ()<GNSWeavePacketHandler>
|
||||
@property(nonatomic) GNSCentralPeerManagerState state;
|
||||
@property(nonatomic) GNSCentralManager *centralManager;
|
||||
@property(nonatomic) dispatch_queue_t queue;
|
||||
@property(nonatomic) int indexOfServiceToCheck;
|
||||
@property(nonatomic) CBService *socketService;
|
||||
@property(nonatomic) CBCharacteristic *outgoingChar;
|
||||
@property(nonatomic) CBCharacteristic *incomingChar;
|
||||
@property(nonatomic) CBCharacteristic *pairingChar;
|
||||
@property(nonatomic, weak) GNSSocket *socket;
|
||||
@property(nonatomic) GNSCentralSocketCompletion discoveringServiceSocketCompletion;
|
||||
@property(nonatomic) BOOL shouldAddPairingCharacteristic;
|
||||
@property(nonatomic) GNSPairingCompletion pairingCompletion;
|
||||
@property(nonatomic) NSMutableArray<GNSReadRRSIValueCompletion> *readRSSIValueCompletions;
|
||||
@property(nonatomic) NSError *disconnectedError;
|
||||
@property(nonatomic) NSData *connectionRequestData;
|
||||
@property(nonatomic) NSDate *startConnectionTime;
|
||||
|
||||
// This timer is scheduled right after the connection request packet is sent, and fires if no
|
||||
// connection confirm packet is received before |kMaxConnectionConfirmWaitTimeInSeconds|.
|
||||
@property(nonatomic) NSTimer *connectionConfirmTimer;
|
||||
|
||||
// The completion of a write-with-response to peripheral; nil if none is in progress.
|
||||
@property(nonatomic) GNSErrorHandler dataWriteCompletion;
|
||||
@end
|
||||
|
||||
@implementation GNSCentralPeerManager
|
||||
|
||||
@synthesize cbPeripheral = _cbPeripheral;
|
||||
|
||||
// Used for testing. A test subclass can override this method so a mock timer can be returned.
|
||||
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval
|
||||
target:(id)target
|
||||
selector:(SEL)selector
|
||||
userInfo:(nullable id)userInfo
|
||||
repeats:(BOOL)yesOrNo {
|
||||
return [NSTimer scheduledTimerWithTimeInterval:timeInterval
|
||||
target:target
|
||||
selector:selector
|
||||
userInfo:userInfo
|
||||
repeats:yesOrNo];
|
||||
}
|
||||
|
||||
- (instancetype)initWithPeripheral:(CBPeripheral *)peripheral
|
||||
centralManager:(GNSCentralManager *)centralManager
|
||||
queue:(dispatch_queue_t)queue {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_cbPeripheral = peripheral;
|
||||
_cbPeripheral.delegate = self;
|
||||
_centralManager = centralManager;
|
||||
_queue = queue;
|
||||
_socketMaximumUpdateValueLength = gGNSCentralPeerManagerMaximumUpdateValue;
|
||||
_state = GNSCentralPeerManagerStateNotConnected;
|
||||
GTMLoggerInfo(@"Peripheral %@", _cbPeripheral);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithPeripheral:(CBPeripheral *)peripheral
|
||||
centralManager:(GNSCentralManager *)centralManager {
|
||||
return [self initWithPeripheral:peripheral
|
||||
centralManager:centralManager
|
||||
queue:dispatch_get_main_queue()];
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
GTMLoggerDebug(@"Dealloc CentralPeerManager with _cbPeripheral %@", _cbPeripheral);
|
||||
_cbPeripheral.delegate = nil;
|
||||
if (_cbPeripheral.state != CBPeripheralStateDisconnected) {
|
||||
[_centralManager cancelPeripheralConnectionForPeer:self];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)socketWithHandshakeData:(NSData *)handshakeData
|
||||
pairingCharacteristic:(BOOL)hasPairingCharacteristic
|
||||
completion:(GNSCentralSocketCompletion)completion {
|
||||
NSAssert(handshakeData.length <= kGNSMaxCentralHandshakeDataSize, @"Handshake data is too large");
|
||||
_connectionRequestData = handshakeData;
|
||||
[self socketWithPairingCharacteristic:hasPairingCharacteristic completion:completion];
|
||||
}
|
||||
|
||||
- (void)socketWithPairingCharacteristic:(BOOL)shouldAddPairingCharacteristics
|
||||
completion:(GNSCentralSocketCompletion)completion {
|
||||
GTMLoggerInfo(@"Request socket %@", self);
|
||||
if (_state != GNSCentralPeerManagerStateNotConnected) {
|
||||
GTMLoggerInfo(@"There is a pending socket request");
|
||||
if (completion) {
|
||||
dispatch_async(_queue, ^{
|
||||
completion(nil, GNSErrorWithCode(GNSErrorOperationInProgress));
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
_shouldAddPairingCharacteristic = shouldAddPairingCharacteristics;
|
||||
_discoveringServiceSocketCompletion = completion;
|
||||
[self bleConnect];
|
||||
}
|
||||
|
||||
- (void)cancelPendingSocket {
|
||||
if (_discoveringServiceSocketCompletion == nil) {
|
||||
GTMLoggerInfo(@"No pending socket, current socket: %@", _socket);
|
||||
return;
|
||||
}
|
||||
GTMLoggerInfo(@"Cancelling pending socket");
|
||||
NSError *cancelPendingSocketRequested = GNSErrorWithCode(GNSErrorCancelPendingSocketRequested);
|
||||
[self disconnectingWithError:cancelPendingSocketRequested];
|
||||
}
|
||||
|
||||
- (void)startBluetoothPairingWithCompletion:(GNSPairingCompletion)completion {
|
||||
NSAssert(_socket, @"Peer socket should have been created.");
|
||||
NSAssert(!_pairingCompletion, @"Pairing already pending.");
|
||||
NSAssert(_pairingChar, @"No a pairing characteristic available.");
|
||||
_pairingCompletion = completion;
|
||||
[_cbPeripheral readValueForCharacteristic:_pairingChar];
|
||||
}
|
||||
|
||||
- (NSUUID *)identifier {
|
||||
return _cbPeripheral.identifier;
|
||||
}
|
||||
|
||||
- (void)readRSSIWithCompletion:(GNSReadRRSIValueCompletion)completion {
|
||||
NSAssert(completion, @"Completion cannot be nil");
|
||||
if (![self isBLEConnected]) {
|
||||
NSAssert(!_readRSSIValueCompletions, @"Should not have pending RSSI completions.");
|
||||
dispatch_async(_queue, ^{ completion(nil, GNSErrorWithCode(GNSErrorNoConnection)); });
|
||||
return;
|
||||
}
|
||||
if (!_readRSSIValueCompletions) {
|
||||
[_cbPeripheral readRSSI];
|
||||
_readRSSIValueCompletions = [NSMutableArray array];
|
||||
}
|
||||
[_readRSSIValueCompletions addObject:completion];
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
return [NSString stringWithFormat:@"<%@: %p, state: \"%@\", peripheral identifier: %@, "
|
||||
@"peripheral state: %@, peripheral: %@>",
|
||||
[self class], self, StateDescription(_state),
|
||||
_cbPeripheral.identifier.UUIDString,
|
||||
PeripheralStateString(_cbPeripheral.state), _cbPeripheral];
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (void)bleConnect {
|
||||
GTMLoggerInfo(@"BLE connection started.");
|
||||
_startConnectionTime = [NSDate date];
|
||||
|
||||
_state = GNSCentralPeerManagerStateBleConnecting;
|
||||
if (_centralManager.cbCentralManagerState == CBCentralManagerStatePoweredOn) {
|
||||
NSDictionary<NSString *, id> *options = @{
|
||||
CBConnectPeripheralOptionNotifyOnDisconnectionKey : @YES,
|
||||
#if TARGET_OS_IPHONE
|
||||
CBConnectPeripheralOptionNotifyOnConnectionKey : @YES,
|
||||
CBConnectPeripheralOptionNotifyOnNotificationKey : @YES,
|
||||
#endif
|
||||
};
|
||||
[_centralManager connectPeripheralForPeer:self options:options];
|
||||
|
||||
// Workaround for an apparent Core Bluetooth problem: Either -didConnectPeripheral or
|
||||
// -didFailToConnectPeripheral should be called at this point, but sometimes neither is called.
|
||||
// In this case, report a timeout error.
|
||||
__weak __typeof__(self) weakSelf = self;
|
||||
dispatch_after(
|
||||
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kPeripheralFailedToConnectTimeout * NSEC_PER_SEC)),
|
||||
_queue, ^{
|
||||
__typeof__(self) strongSelf = weakSelf;
|
||||
if (!strongSelf) return;
|
||||
if (strongSelf.state == GNSCentralPeerManagerStateBleConnecting) {
|
||||
GTMLoggerInfo(@"Timed out trying to connect to peripheral: %@", strongSelf.cbPeripheral);
|
||||
[strongSelf.centralManager cancelPeripheralConnectionForPeer:self];
|
||||
[strongSelf bleDisconnectedWithError:GNSErrorWithCode(GNSErrorConnectionTimedOut)];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)bleConnected {
|
||||
if (_cbPeripheral.state != CBPeripheralStateConnected) {
|
||||
GTMLoggerInfo(@"Unexpected peripheral state: %ld", (long)_cbPeripheral.state);
|
||||
}
|
||||
GTMLoggerInfo(@"BLE connected. Elapsed time: %f",
|
||||
[[NSDate date] timeIntervalSinceDate:_startConnectionTime]);
|
||||
if (_state != GNSCentralPeerManagerStateBleConnecting) {
|
||||
GTMLoggerInfo(@"Should not be connected with %@ when in %@ state", _cbPeripheral,
|
||||
StateDescription(_state));
|
||||
return;
|
||||
}
|
||||
GTMLoggerInfo(@"BLE connected, peripheral: %@", _cbPeripheral);
|
||||
[self discoverService];
|
||||
}
|
||||
|
||||
- (void)bleDisconnectedWithError:(NSError *)error {
|
||||
GTMLoggerInfo(@"BLE disconnected, peripheral: %@", _cbPeripheral);
|
||||
if (_state == GNSCentralPeerManagerStateNotConnected) {
|
||||
return;
|
||||
} else {
|
||||
if (!_disconnectedError) {
|
||||
_disconnectedError = error;
|
||||
}
|
||||
[_connectionConfirmTimer invalidate];
|
||||
_connectionConfirmTimer = nil;
|
||||
[self bleDisconnected];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)cbCentralManagerStateDidUpdate {
|
||||
if (_centralManager.cbCentralManagerState == CBCentralManagerStatePoweredOn &&
|
||||
_state == GNSCentralPeerManagerStateBleConnecting) {
|
||||
[self bleConnect];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)disconnectingWithError:(NSError *)error {
|
||||
if (error) {
|
||||
GTMLoggerInfo(@"Disconnected with error: %@, peripheral: %@", error, _cbPeripheral);
|
||||
} else {
|
||||
GTMLoggerInfo(@"Disconnected from peripheral: %@", _cbPeripheral);
|
||||
}
|
||||
[_connectionConfirmTimer invalidate];
|
||||
_connectionConfirmTimer = nil;
|
||||
_connectionRequestData = nil;
|
||||
_disconnectedError = error;
|
||||
|
||||
// If there is a pending characteristic write, call the completion.
|
||||
[self callDataWriteCompletionWithError:GNSErrorWithCode(GNSErrorLostConnection)];
|
||||
|
||||
[self cleanRSSICompletionAfterDisconnectionWithError:error];
|
||||
_state = GNSCentralPeerManagerStateBleDisconnecting;
|
||||
if (_cbPeripheral.state != CBPeripheralStateDisconnected) {
|
||||
[_centralManager cancelPeripheralConnectionForPeer:self];
|
||||
} else {
|
||||
[self bleDisconnected];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)bleDisconnected {
|
||||
GTMLoggerInfo(@"BLE Disconnected %@", _cbPeripheral);
|
||||
if (_cbPeripheral.state != CBPeripheralStateDisconnected) {
|
||||
GTMLoggerInfo(@"Unexpected peripheral state: %ld", (long)_cbPeripheral.state);
|
||||
}
|
||||
NSAssert(_cbPeripheral.delegate == self, @"Self = %@ should be the delegate of %@", self,
|
||||
_cbPeripheral);
|
||||
|
||||
// Clean-up this peer manager.
|
||||
[_centralManager centralPeerManagerDidDisconnect:self];
|
||||
GNSSocket *currentSocket = _socket;
|
||||
_socketService = nil;
|
||||
_socket = nil;
|
||||
_state = GNSCentralPeerManagerStateNotConnected;
|
||||
|
||||
// Inform all interested parties that the peripheral is disconnected.
|
||||
[self cleanRSSICompletionAfterDisconnectionWithError:_disconnectedError];
|
||||
if (_discoveringServiceSocketCompletion) {
|
||||
NSAssert(!_socket, @"Should not have a socket yet.");
|
||||
if (!_disconnectedError) {
|
||||
_disconnectedError = GNSErrorWithCode(GNSErrorNoConnection);
|
||||
}
|
||||
GNSCentralSocketCompletion completion = _discoveringServiceSocketCompletion;
|
||||
_discoveringServiceSocketCompletion = nil;
|
||||
dispatch_async(_queue, ^{ completion(nil, _disconnectedError); });
|
||||
} else if (currentSocket) {
|
||||
[currentSocket didDisconnectWithError:_disconnectedError];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)discoverService {
|
||||
_state = GNSCentralPeerManagerStateDiscoveringService;
|
||||
CBUUID *serviceUUID = _centralManager.socketServiceUUID;
|
||||
GTMLoggerInfo(@"Discover service %@ on peripheral: %@", serviceUUID, _cbPeripheral);
|
||||
NSArray<CBUUID *> *servicesToDiscover = @[ serviceUUID ];
|
||||
[_cbPeripheral discoverServices:servicesToDiscover];
|
||||
}
|
||||
|
||||
- (void)discoverCharacteristics {
|
||||
if (_cbPeripheral.services.count == 0) return;
|
||||
GTMLoggerInfo(@"Discover characteristics, peer manager %@", self);
|
||||
NSMutableArray<CBUUID *> *characteristics = [NSMutableArray
|
||||
arrayWithObjects:[CBUUID UUIDWithString:kGNSWeaveToPeripheralCharUUIDString],
|
||||
[CBUUID UUIDWithString:kGNSWeaveFromPeripheralCharUUIDString], nil];
|
||||
if (_shouldAddPairingCharacteristic) {
|
||||
[characteristics addObject:[CBUUID UUIDWithString:kGNSPairingCharUUIDString]];
|
||||
}
|
||||
[_cbPeripheral discoverCharacteristics:characteristics
|
||||
forService:_cbPeripheral.services[_indexOfServiceToCheck]];
|
||||
}
|
||||
|
||||
- (BOOL)findCharacteristicsFromService:(CBService *)service {
|
||||
CBCharacteristic *outgoingChar = nil;
|
||||
CBCharacteristic *incomingChar = nil;
|
||||
CBCharacteristic *pairingChar = nil;
|
||||
for (CBCharacteristic *characteristic in service.characteristics) {
|
||||
if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:kGNSWeaveToPeripheralCharUUIDString]]) {
|
||||
outgoingChar = characteristic;
|
||||
} else if ([characteristic.UUID
|
||||
isEqual:[CBUUID UUIDWithString:kGNSWeaveFromPeripheralCharUUIDString]]) {
|
||||
incomingChar = characteristic;
|
||||
} else if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:kGNSPairingCharUUIDString]]) {
|
||||
pairingChar = characteristic;
|
||||
}
|
||||
}
|
||||
if (!outgoingChar || !incomingChar || (_shouldAddPairingCharacteristic && !pairingChar)) {
|
||||
return NO;
|
||||
}
|
||||
_incomingChar = incomingChar;
|
||||
_outgoingChar = outgoingChar;
|
||||
_pairingChar = pairingChar;
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)setCharacteristicNotification {
|
||||
NSAssert(_incomingChar, @"Should have found incoming characteristic");
|
||||
_state = GNSCentralPeerManagerStateSettingNotifications;
|
||||
[_cbPeripheral setNotifyValue:YES forCharacteristic:_incomingChar];
|
||||
}
|
||||
|
||||
- (CBPeripheral *)cbPeripheral {
|
||||
return _cbPeripheral;
|
||||
}
|
||||
|
||||
// If error is nil, kGNSNoConnection |error| is passed to the rssi completion.
|
||||
- (void)cleanRSSICompletionAfterDisconnectionWithError:(NSError *)error {
|
||||
if (_readRSSIValueCompletions) {
|
||||
NSError *rssiCompletionError = error;
|
||||
if (rssiCompletionError) {
|
||||
rssiCompletionError = GNSErrorWithCode(GNSErrorNoConnection);
|
||||
}
|
||||
[self callRSSICompletionWithRSSIValue:nil error:rssiCompletionError];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)callRSSICompletionWithRSSIValue:(NSNumber *)rssiValue error:(NSError *)error {
|
||||
NSArray<GNSReadRRSIValueCompletion> *completions = _readRSSIValueCompletions;
|
||||
_readRSSIValueCompletions = nil;
|
||||
for (GNSReadRRSIValueCompletion completion in completions) {
|
||||
dispatch_async(_queue, ^{ completion(rssiValue, error); });
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)isBLEConnected {
|
||||
switch (_state) {
|
||||
case GNSCentralPeerManagerStateNotConnected:
|
||||
case GNSCentralPeerManagerStateBleConnecting:
|
||||
return NO;
|
||||
case GNSCentralPeerManagerStateBleConnected:
|
||||
case GNSCentralPeerManagerStateDiscoveringService:
|
||||
case GNSCentralPeerManagerStateDiscoveringCharacteristic:
|
||||
case GNSCentralPeerManagerStateSettingNotifications:
|
||||
case GNSCentralPeerManagerStateSocketCommunication:
|
||||
case GNSCentralPeerManagerStateBleDisconnecting:
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)timeOutConnectionForTimer:(NSTimer *)timer {
|
||||
NSAssert(_state == GNSCentralPeerManagerStateSocketCommunication,
|
||||
@"Timer (%@) fired on the wrong state. [self = %@]", _connectionConfirmTimer, self);
|
||||
GTMLoggerInfo(@"Timing out %@ socket connection [self = %@].", _socket, self);
|
||||
_connectionConfirmTimer = nil;
|
||||
NSError *error = GNSErrorWithCode(GNSErrorConnectionTimedOut);
|
||||
[self disconnectingWithError:error];
|
||||
}
|
||||
|
||||
- (void)callDataWriteCompletionWithError:(NSError *)error {
|
||||
if (_dataWriteCompletion) {
|
||||
GNSErrorHandler dataWriteCompletion = _dataWriteCompletion; // tail call for reentrancy
|
||||
_dataWriteCompletion = nil;
|
||||
dispatch_async(_queue, ^{ dataWriteCompletion(error); });
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - CBPeripheralDelegate
|
||||
|
||||
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error {
|
||||
GTMLoggerInfo(@"BLE services discovered. Elapsed time: %f",
|
||||
[[NSDate date] timeIntervalSinceDate:_startConnectionTime]);
|
||||
if (_state != GNSCentralPeerManagerStateDiscoveringService) {
|
||||
GTMLoggerInfo(@"Ignoring services discovered from %@ when in %@ state", _cbPeripheral,
|
||||
StateDescription(_state));
|
||||
return;
|
||||
}
|
||||
GTMLoggerInfo(@"Service discovered for %@, error: %@", self, error);
|
||||
if (error) {
|
||||
[self disconnectingWithError:error];
|
||||
return;
|
||||
}
|
||||
|
||||
// Multiple services may have the same UUID, so find the socket service by checking for the
|
||||
// expected characteristics.
|
||||
_indexOfServiceToCheck = 0;
|
||||
_state = GNSCentralPeerManagerStateDiscoveringCharacteristic;
|
||||
[self discoverCharacteristics];
|
||||
}
|
||||
|
||||
- (void)peripheral:(CBPeripheral *)peripheral
|
||||
didDiscoverCharacteristicsForService:(CBService *)service
|
||||
error:(NSError *)error {
|
||||
GTMLoggerInfo(@"BLE characteristics discovered. Elapsed time: %f",
|
||||
[[NSDate date] timeIntervalSinceDate:_startConnectionTime]);
|
||||
if (_state != GNSCentralPeerManagerStateDiscoveringCharacteristic) {
|
||||
GTMLoggerInfo(@"Ignoring characteristics discovered from %@ when in %@ state", _cbPeripheral,
|
||||
StateDescription(_state));
|
||||
return;
|
||||
}
|
||||
GTMLoggerInfo(@"Characteristics discovered for service: %@, error: %@", service, error);
|
||||
CBService *serviceBeingChecked = _cbPeripheral.services[_indexOfServiceToCheck];
|
||||
NSAssert([service.UUID isEqual:serviceBeingChecked.UUID], @"Unknown service %@ %@",
|
||||
service, serviceBeingChecked.UUID);
|
||||
if (error) {
|
||||
[self disconnectingWithError:error];
|
||||
return;
|
||||
}
|
||||
if (![self findCharacteristicsFromService:serviceBeingChecked]) {
|
||||
if (++_indexOfServiceToCheck == _cbPeripheral.services.count) {
|
||||
// Didn't find the characteristics in any of the services.
|
||||
NSError *missingCharacteristicError = GNSErrorWithCode(GNSErrorMissingCharacteristics);
|
||||
[self disconnectingWithError:missingCharacteristicError];
|
||||
} else {
|
||||
// Check the next service in the list.
|
||||
[self discoverCharacteristics];
|
||||
}
|
||||
return;
|
||||
}
|
||||
[self setCharacteristicNotification];
|
||||
}
|
||||
|
||||
- (void)handleWeaveError:(GNSError)errorCode socket:(GNSSocket *)socket {
|
||||
if (!socket) {
|
||||
return;
|
||||
}
|
||||
NSAssert(socket == _socket, @"Wrong socket, socket = %@, _socket = %@.", socket, _socket);
|
||||
NSError *error = GNSErrorWithCode(errorCode);
|
||||
// Only send the error if we didn't already received an error packet.
|
||||
if (errorCode != GNSErrorWeaveErrorPacketReceived) {
|
||||
GNSWeaveErrorPacket *errorPacket =
|
||||
[[GNSWeaveErrorPacket alloc] initWithPacketCounter:socket.sendPacketCounter];
|
||||
[self sendPacket:errorPacket];
|
||||
}
|
||||
[self disconnectingWithError:error];
|
||||
}
|
||||
|
||||
- (void)peripheral:(CBPeripheral *)peripheral
|
||||
didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic
|
||||
error:(NSError *)error {
|
||||
if (_state != GNSCentralPeerManagerStateSocketCommunication) {
|
||||
GTMLoggerInfo(@"Ignoring data received from %@ when in %@ state", _cbPeripheral,
|
||||
StateDescription(_state));
|
||||
return;
|
||||
}
|
||||
NSAssert(peripheral == _cbPeripheral, @"Unknown peripheral.");
|
||||
if ([characteristic.UUID isEqual:_pairingChar.UUID]) {
|
||||
GNSPairingCompletion completion = _pairingCompletion;
|
||||
_pairingCompletion = nil;
|
||||
if (completion) dispatch_async(_queue, ^{ completion(error == nil, error); });
|
||||
return;
|
||||
}
|
||||
NSAssert(characteristic == _incomingChar, @"Should read only from the incoming characteristic.");
|
||||
NSAssert(_socket, @"Should have a socket in the %@ state", StateDescription(_state));
|
||||
NSData *charValue = characteristic.value;
|
||||
// Truncate the packet to expected size: |kGNSMinSupportedPacketSize| for the first packet and
|
||||
// |socket.packetSize| for the rest.
|
||||
NSUInteger truncatedSize = MIN(_socket.packetSize, charValue.length);
|
||||
if (truncatedSize != charValue.length) {
|
||||
GTMLoggerInfo(@"Packet with %ld bytes trucanted to %ld", (long)charValue.length,
|
||||
(long)truncatedSize);
|
||||
}
|
||||
NSData *packetData = [charValue subdataWithRange:NSMakeRange(0, truncatedSize)];
|
||||
NSError *parsingError = nil;
|
||||
GNSWeavePacket *packet = [GNSWeavePacket parseData:packetData error:&parsingError];
|
||||
if (!packet) {
|
||||
GTMLoggerError(@"Error parsing Weave packet (error = %@).", parsingError);
|
||||
[self handleWeaveError:GNSErrorParsingWeavePacket socket:_socket];
|
||||
return;
|
||||
}
|
||||
if (packet.packetCounter != _socket.receivePacketCounter) {
|
||||
GTMLoggerError(@"Wrong packet counter, [received %d, expected %d].", packet.packetCounter,
|
||||
_socket.receivePacketCounter);
|
||||
[self handleWeaveError:GNSErrorWrongWeavePacketCounter socket:_socket];
|
||||
return;
|
||||
}
|
||||
[packet visitWithHandler:self context:nil];
|
||||
[_socket incrementReceivePacketCounter];
|
||||
}
|
||||
|
||||
- (void)peripheral:(CBPeripheral *)peripheral
|
||||
didWriteValueForCharacteristic:(CBCharacteristic *)characteristic
|
||||
error:(NSError *)error {
|
||||
// Note: Avoid using |characteristic.value| here as it seems it is always nil.
|
||||
if (error) {
|
||||
GTMLoggerInfo(@"Characteristic write failed with error: %@", error);
|
||||
[self disconnectingWithError:error];
|
||||
} else {
|
||||
GTMLoggerInfo(@"Characteristic write succeeded");
|
||||
}
|
||||
[self callDataWriteCompletionWithError:error];
|
||||
}
|
||||
|
||||
// This method sends |packet| fitting a single characteristic write to |socket|. All packets sent by
|
||||
// this class (not the socket) must use this method.
|
||||
- (void)sendPacket:(GNSWeavePacket *)packet {
|
||||
GTMLoggerInfo(@"Writing value to characteristic (internal)");
|
||||
NSAssert(packet.packetCounter == _socket.sendPacketCounter, @"Wrong packet counter.");
|
||||
[_cbPeripheral writeValue:[packet serialize]
|
||||
forCharacteristic:_outgoingChar
|
||||
type:CBCharacteristicWriteWithResponse];
|
||||
[_socket incrementSendPacketCounter];
|
||||
}
|
||||
|
||||
- (void)peripheral:(CBPeripheral *)peripheral
|
||||
didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic
|
||||
error:(NSError *)error {
|
||||
GTMLoggerInfo(@"BLE characteristic notifications started. Elapsed time: %f",
|
||||
[[NSDate date] timeIntervalSinceDate:_startConnectionTime]);
|
||||
if (_state != GNSCentralPeerManagerStateSettingNotifications) {
|
||||
GTMLoggerInfo(@"Ignoring notification status change for %@ when in %@ state", _cbPeripheral,
|
||||
StateDescription(_state));
|
||||
return;
|
||||
}
|
||||
GTMLoggerInfo(@"Characteristic notification update: %@, error: %@", characteristic, error);
|
||||
if (error) {
|
||||
[self disconnectingWithError:error];
|
||||
return;
|
||||
}
|
||||
NSAssert([characteristic.UUID isEqual:_incomingChar.UUID], @"Wrong characteristic");
|
||||
NSAssert(_state == GNSCentralPeerManagerStateSettingNotifications, @"Wrong state");
|
||||
_state = GNSCentralPeerManagerStateSocketCommunication;
|
||||
GTMLoggerInfo(@"Socket ready %@", self);
|
||||
GNSSocket *socket =
|
||||
[[GNSSocket alloc] initWithOwner:self peripheralPeer:_cbPeripheral queue:_queue];
|
||||
GNSCentralSocketCompletion completion = _discoveringServiceSocketCompletion;
|
||||
_discoveringServiceSocketCompletion = nil;
|
||||
_socket = socket;
|
||||
dispatch_async(_queue, ^{ completion(socket, nil); });
|
||||
if (!_socket) {
|
||||
[self disconnectingWithError:nil];
|
||||
return;
|
||||
}
|
||||
GTMLoggerInfo(@"Sending connection request packet.");
|
||||
// On iOS/OS X the central (client) doesn't have access to the negotiated BLE connection MTU. So,
|
||||
// according to the Weave BLE protocol specs, it should send 0. The peripheral will then choose
|
||||
// the appropriated value for the packet size and send it back on the connection confirm packet.
|
||||
GNSWeaveConnectionRequestPacket *connectionRequest =
|
||||
[[GNSWeaveConnectionRequestPacket alloc] initWithMinVersion:kWeaveVersionSupported
|
||||
maxVersion:kWeaveVersionSupported
|
||||
maxPacketSize:0
|
||||
data:_connectionRequestData];
|
||||
[self sendPacket:connectionRequest];
|
||||
_connectionConfirmTimer =
|
||||
[[self class] scheduledTimerWithTimeInterval:kMaxConnectionConfirmWaitTimeInSeconds
|
||||
target:self
|
||||
selector:@selector(timeOutConnectionForTimer:)
|
||||
userInfo:nil
|
||||
repeats:NO];
|
||||
}
|
||||
|
||||
- (void)peripheral:(CBPeripheral *)peripheral didReadRSSI:(NSNumber *)RSSI error:(NSError *)error {
|
||||
if (error) {
|
||||
GTMLoggerError(@"Error to read RSSI %@", error);
|
||||
}
|
||||
[self callRSSICompletionWithRSSIValue:RSSI error:error];
|
||||
}
|
||||
|
||||
#pragma mark - GNSSocketOwner
|
||||
|
||||
- (NSUInteger)socketMaximumUpdateValueLength:(GNSSocket *)socket {
|
||||
return _socketMaximumUpdateValueLength;
|
||||
}
|
||||
|
||||
- (void)sendData:(NSData *)data socket:(GNSSocket *)socket completion:(GNSErrorHandler)completion {
|
||||
GTMLoggerInfo(@"Writing value to characteristic");
|
||||
if (_dataWriteCompletion != nil) {
|
||||
// This shouldn't happen because writes should be serialized by the socket code. But log it
|
||||
// in case there's a bug that causes it to happen.
|
||||
GTMLoggerInfo(@"Previous characteristic data write didn't complete");
|
||||
}
|
||||
|
||||
// Sometimes -didWriteValueForCharacteristic: isn't called, leaving the write operation hanging.
|
||||
// If it times out, call the completion with an error.
|
||||
__weak __typeof__(self) weakSelf = self;
|
||||
dispatch_block_t dataWriteTimeoutBlock = dispatch_block_create(0, ^{
|
||||
__typeof__(self) strongSelf = weakSelf;
|
||||
if (!strongSelf || !socket.isConnected) return;
|
||||
GTMLoggerInfo(@"Characteristic data write timed out");
|
||||
[strongSelf callDataWriteCompletionWithError:GNSErrorWithCode(GNSErrorConnectionTimedOut)];
|
||||
});
|
||||
_dataWriteCompletion = ^(NSError *error) {
|
||||
// -didWriteValueForCharacteristic: was called, so cancel the timeout
|
||||
dispatch_block_cancel(dataWriteTimeoutBlock);
|
||||
completion(error);
|
||||
};
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
|
||||
(int64_t)(kChracteristicWriteTimeoutInSeconds * NSEC_PER_SEC)),
|
||||
_queue,
|
||||
dataWriteTimeoutBlock);
|
||||
|
||||
[_cbPeripheral writeValue:data
|
||||
forCharacteristic:_outgoingChar
|
||||
type:CBCharacteristicWriteWithResponse];
|
||||
}
|
||||
|
||||
- (NSUUID *)socketServiceIdentifier:(GNSSocket *)socket {
|
||||
return _cbPeripheral.identifier;
|
||||
}
|
||||
|
||||
- (void)disconnectSocket:(GNSSocket *)socket {
|
||||
if (!socket.isConnected || _state == GNSCentralPeerManagerStateNotConnected ||
|
||||
_state == GNSCentralPeerManagerStateBleDisconnecting) {
|
||||
return;
|
||||
}
|
||||
GTMLoggerInfo(@"Disconnect socket %@", socket);
|
||||
[self disconnectingWithError:nil];
|
||||
}
|
||||
|
||||
- (void)socketWillBeDeallocated:(GNSSocket *)socket {
|
||||
if ((_state == GNSCentralPeerManagerStateBleDisconnecting) ||
|
||||
(_state == GNSCentralPeerManagerStateNotConnected)) {
|
||||
// Already disconnecting or disconnected. Nothing to do.
|
||||
return;
|
||||
}
|
||||
// The socket owner is dropping the socket without notice. The BLE has to be disconnected.
|
||||
_socket = nil;
|
||||
[self disconnectingWithError:nil];
|
||||
}
|
||||
|
||||
#pragma mark - GNSWeavePacketHandler
|
||||
|
||||
- (void)handleConnectionRequestPacket:(GNSWeaveConnectionRequestPacket *)packet
|
||||
context:(id)context {
|
||||
GTMLoggerError(@"Unexpected connection request packet received.");
|
||||
[self handleWeaveError:GNSErrorUnexpectedWeaveControlPacket socket:_socket];
|
||||
}
|
||||
|
||||
- (void)handleConnectionConfirmPacket:(GNSWeaveConnectionConfirmPacket *)packet
|
||||
context:(id)context {
|
||||
// The protocol specs guarantee that a connection confirm packet should contain a valid
|
||||
// version for the client, i.e. inside the version interval sent in the connection request
|
||||
// packet. If the server doesn't support any versions in the client interval an error packet
|
||||
// should be sent, not a connection confirm.
|
||||
NSAssert(packet.version == kWeaveVersionSupported, @"Unsupported Weave protocol version: %d.",
|
||||
packet.version);
|
||||
[_connectionConfirmTimer invalidate];
|
||||
_connectionConfirmTimer = nil;
|
||||
_socket.packetSize = packet.packetSize;
|
||||
[_socket didConnect];
|
||||
if (packet.data) {
|
||||
// According to the Weave BLE protocol the data received during the connection handshake should
|
||||
// be treated as the first message of the socket.
|
||||
//
|
||||
// Simulate a data packet being received. The packet counter value is not checked by the socket.
|
||||
GNSWeaveDataPacket *dataPacket = [[GNSWeaveDataPacket alloc] initWithPacketCounter:0
|
||||
firstPacket:YES
|
||||
lastPacket:YES
|
||||
data:packet.data];
|
||||
[_socket didReceiveIncomingWeaveDataPacket:dataPacket];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleErrorPacket:(GNSWeaveErrorPacket *)packet context:(id)context {
|
||||
GTMLoggerInfo(@"Error packet received.");
|
||||
[self handleWeaveError:GNSErrorWeaveErrorPacketReceived socket:_socket];
|
||||
}
|
||||
|
||||
- (void)handleDataPacket:(GNSWeaveDataPacket *)packet context:(id)context {
|
||||
if (packet.isFirstPacket && _socket.waitingForIncomingData) {
|
||||
GTMLoggerError(@"There is already a receive operation in progress");
|
||||
[self handleWeaveError:GNSErrorWeaveDataTransferInProgress socket:_socket];
|
||||
return;
|
||||
}
|
||||
[_socket didReceiveIncomingWeaveDataPacket:packet];
|
||||
}
|
||||
|
||||
#pragma mark - TestingHelpers
|
||||
|
||||
- (NSTimer *)testing_connectionConfirmTimer {
|
||||
return _connectionConfirmTimer;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralManager.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Central/GNSCentralPeerManager.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/GNSShared.h"
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/GNSShared.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralManager.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralServiceManager.h"
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSUtils.h"
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralManager.h"
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket+Private.h"
|
||||
|
||||
@class GNSSocket;
|
||||
|
||||
typedef BOOL (^GNSUpdateValueHandler)();
|
||||
|
||||
/**
|
||||
* Private methods called by GNSPeripheralManager, GNSSocket and for tests.
|
||||
* Should not be used by the Nearby Socket client.
|
||||
*/
|
||||
@interface GNSPeripheralManager ()<CBPeripheralManagerDelegate>
|
||||
|
||||
@property(nonatomic, readonly) NSString *restoreIdentifier;
|
||||
@property(nonatomic, readonly) CBPeripheralManager *cbPeripheralManager;
|
||||
|
||||
/**
|
||||
* Updates the outgoing characteristic value using an handler. The handler is stored in
|
||||
* a queue. If the CBPeripheralManager is ready, the handler is called right away. Otherwise the
|
||||
* handler will be called as soon as the CBPeripheralManager is ready.
|
||||
* The characteristic value should be updated with
|
||||
* -[GNSPeripheralManager updateValue:forCharacteristic:onSocket:]. If the update failed,
|
||||
* the handler should return NO, and the handler will be rescheduled when the peripheral will ready
|
||||
* again.
|
||||
*
|
||||
* @param socket Socket
|
||||
* @param handler Handler to update the value. Should return YES if the data was sent successfully,
|
||||
* and NO if the update should be scheduled for later.
|
||||
*/
|
||||
- (void)updateOutgoingCharOnSocket:(GNSSocket *)socket withHandler:(GNSUpdateValueHandler)handler;
|
||||
|
||||
/**
|
||||
* Stops BLE advertising and starts it again with the peripheral service managers who are set to be
|
||||
* advertising.
|
||||
*/
|
||||
- (void)updateAdvertisedServices;
|
||||
|
||||
/**
|
||||
* Sends a packet using the outgoing characteristic for a socket.
|
||||
*
|
||||
* @param data Packet to send
|
||||
* @param socket Socket to send the packet
|
||||
*
|
||||
* @return YES if the packet has been sent.
|
||||
*/
|
||||
- (BOOL)updateOutgoingCharacteristic:(NSData *)data onSocket:(GNSSocket *)socket;
|
||||
|
||||
/**
|
||||
* Informs the peripheral manager that |socket| is now disconnected.
|
||||
*
|
||||
* @param socket Socket.
|
||||
*/
|
||||
- (void)socketDidDisconnect:(GNSSocket *)socket;
|
||||
|
||||
@end
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import <CoreBluetooth/CoreBluetooth.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSUtils.h"
|
||||
|
||||
@class GNSPeripheralServiceManager;
|
||||
|
||||
/**
|
||||
* This class is in charge of the CBPeripheralManager. It owns the list of
|
||||
* GNSPeripheralServiceManager to advertise. This class dispatches events received from
|
||||
* the CBPeripheralManager.
|
||||
*
|
||||
* * To create a GNSPeripheralManager instance:
|
||||
* GNSPeripheralManager *peripheralManager =
|
||||
* [[GNSPeripheralManager alloc] initWithAdvertisedName:@"AdvertisedName"
|
||||
* restoreIdentifier:@"BLEIdentifier"];
|
||||
*
|
||||
* * To create a service:
|
||||
* GNSShouldAcceptSocketHandler shouldAcceptSocketHandler = ^(GNSSocket *socket) {
|
||||
* socket.delegate = mySocketDelegate;
|
||||
* return YES;
|
||||
* }
|
||||
* GNSPeripheralServiceManager *service =
|
||||
* [[GNSPeripheralServiceManager alloc] initWithBleServiceUUID:serviceUUID
|
||||
* shouldAcceptSocketHandler:shouldAcceptSocketHandler];
|
||||
* [peripheralManager addPeripheralServiceManager:service bleServiceAddedCompletion:handler];
|
||||
*
|
||||
*
|
||||
* * When a socket is created, |shouldAcceptSocketHandler| is called. If the handler returns YES,
|
||||
* the socket delegate should be set, and the socket should be retained. The socket delegate
|
||||
* will be called as soon as the socket is connected. If the handler returns NO, the socket
|
||||
* should not be used.
|
||||
* See GNSSocket documentation to send and receive data.
|
||||
*
|
||||
* This class is not thread-safe.
|
||||
*/
|
||||
@interface GNSPeripheralManager : NSObject
|
||||
|
||||
@property(nonatomic, readonly, getter=isStarted) BOOL started;
|
||||
@property(nonatomic, readonly) CBManagerState peripheralManagerState;
|
||||
@property(nonatomic, copy) GNSPeripheralManagerStateHandler peripheralManagerStateHandler;
|
||||
|
||||
/**
|
||||
* Display name used in the BLE advertisement.
|
||||
*
|
||||
* Note that the change of the advertisement name will only be effective on the next change of
|
||||
* the advertised service UUIDs.
|
||||
*/
|
||||
@property(nonatomic, copy) NSString *advertisedName;
|
||||
|
||||
/**
|
||||
* Creates a CBPeripheralManager instance and sets itself as the delegate. Pass nil to
|
||||
* |advertisedName| to avoid setting the BLE advertised name; this is useful for avoiding a name
|
||||
* collision with clients that use the name in a custom discovery algorithm.
|
||||
*
|
||||
* @param advertisedName Display name used in the BLE advertisement
|
||||
* @param restoreIdentifier Value used for CBPeripheralManagerOptionRestoreIdentifierKey
|
||||
* @param queue The queue this object is called on and callbacks are made on.
|
||||
*
|
||||
* @return GNSPeripheralManager instance
|
||||
*/
|
||||
- (instancetype)initWithAdvertisedName:(NSString *)advertisedName
|
||||
restoreIdentifier:(NSString *)restoreIdentifier
|
||||
queue:(dispatch_queue_t)queue NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
/**
|
||||
* Creates a CBPeripheralManager using the main queue for callbacks.
|
||||
*/
|
||||
- (instancetype)initWithAdvertisedName:(NSString *)advertisedName
|
||||
restoreIdentifier:(NSString *)restoreIdentifier;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
/**
|
||||
* Adds a new peripheral service manager into the managed services. If the GNSPeripheralManager
|
||||
* is started, the new peripheral service manager will be added right away into BLE service
|
||||
* database. |completion| is called everytime the service is added into the BLE service database.
|
||||
*
|
||||
* @param peripheralServiceManager GNSPeripheralServiceManager
|
||||
* @param completion Callback called when the service was added.
|
||||
*/
|
||||
- (void)addPeripheralServiceManager:(GNSPeripheralServiceManager *)peripheralServiceManager
|
||||
bleServiceAddedCompletion:(GNSErrorHandler)completion;
|
||||
|
||||
/**
|
||||
* If the bluetooth is on, all CB services will be added, and the services will be advertised
|
||||
* (according to -[GNSPeripheralServiceManager advertising]. Otherwise, it will be done as soon as
|
||||
* the bluetooth is turned on.
|
||||
*/
|
||||
- (void)start;
|
||||
|
||||
/**
|
||||
* Bluetooth advertisement is turned off, and all services are removed from CB.
|
||||
*/
|
||||
- (void)stop;
|
||||
|
||||
@end
|
||||
+546
@@ -0,0 +1,546 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralManager+Private.h"
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralServiceManager+Private.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket+Private.h"
|
||||
#import "GoogleToolboxForMac/GTMLogger.h"
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
#import <UIKit/UIKit.h>
|
||||
#endif
|
||||
|
||||
static NSString *CBManagerStateString(CBManagerState state) {
|
||||
switch (state) {
|
||||
case CBManagerStateUnknown:
|
||||
return @"CBManagerStateUnknown";
|
||||
case CBManagerStateResetting:
|
||||
return @"CBManagerStateResetting";
|
||||
case CBManagerStateUnsupported:
|
||||
return @"CBManagerStateUnsupported";
|
||||
case CBManagerStateUnauthorized:
|
||||
return @"CBManagerStateUnauthorized";
|
||||
case CBManagerStatePoweredOff:
|
||||
return @"CBManagerStatePoweredOff";
|
||||
case CBManagerStatePoweredOn:
|
||||
return @"CBManagerStatePoweredOn";
|
||||
}
|
||||
return @"CBManagerState Unknown";
|
||||
}
|
||||
|
||||
// http://b/28875581 On iOS, under some misterious conditions, the Bluetooth daemon (BTServer
|
||||
// process) continously crashes when attempting to register Bluetooth services. In fact, if the
|
||||
// BTServer process is killed, the OS spins a new instace of this process after 10 seconds.
|
||||
//
|
||||
// It is defined as a resetting episode a set of successive state changes of a CBPeripheralManager
|
||||
// that include at least one state CBManagerStateResetting.
|
||||
// As a heuristic, a resetting episode is considered a crash loop of the Bluetooth daemon iff:
|
||||
// * the CBPeripheralManager enters more than 5 times in CBManagerStateResetting;
|
||||
// * the time interval between successive CBManagerStateResetting notifications is
|
||||
// less than 15 seconds;
|
||||
//
|
||||
// The minimum number of successive CBManagerStateResetting states for a resetting
|
||||
// episode to be considered a Bluetooth daemon crash loop.
|
||||
static NSInteger gKBTCrashLoopMinNumberOfResetting = 5;
|
||||
|
||||
// The maximum time interval between successive CBManagerStateResetting states for a
|
||||
// resetting episode to be considered a Bluetooth daemon crash loop.
|
||||
static NSTimeInterval gKBTCrashLoopMaxTimeBetweenResetting = 15.f;
|
||||
|
||||
@interface GNSPeripheralManager () {
|
||||
// key: -[GNSPeripheralServiceManager serviceUUID], value: GNSPeripheralServiceManager
|
||||
NSMapTable *_peripheralServiceManagers;
|
||||
// Queue of blocks to update BLE values
|
||||
NSMutableDictionary<NSUUID *, NSMutableArray<GNSUpdateValueHandler> *>
|
||||
*_handlerQueuePerSocketIdentifier;
|
||||
NSDictionary<NSString *, id> *_advertisementInProgressData;
|
||||
NSDictionary<NSString *, id> *_advertisementData;
|
||||
dispatch_queue_t _queue;
|
||||
|
||||
// Bluetooth daemon crash info.
|
||||
NSDate *_btCrashLastResettingDate;
|
||||
NSInteger _btCrashCount;
|
||||
}
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
@property(nonatomic, assign) UIBackgroundTaskIdentifier backgroundTaskId;
|
||||
#endif
|
||||
|
||||
@property(nonatomic, getter=isStarted) BOOL started;
|
||||
|
||||
@end
|
||||
|
||||
@implementation GNSPeripheralManager
|
||||
|
||||
- (instancetype)init {
|
||||
[self doesNotRecognizeSelector:_cmd];
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (instancetype)initWithAdvertisedName:(NSString *)advertisedName
|
||||
restoreIdentifier:(NSString *)restoreIdentifier
|
||||
queue:(dispatch_queue_t)queue {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_restoreIdentifier = [restoreIdentifier copy];
|
||||
_advertisedName = [advertisedName copy];
|
||||
_handlerQueuePerSocketIdentifier = [NSMutableDictionary dictionary];
|
||||
_peripheralServiceManagers = [NSMapTable strongToWeakObjectsMapTable];
|
||||
_btCrashLastResettingDate = [NSDate dateWithTimeIntervalSince1970:0];
|
||||
_queue = queue;
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
_backgroundTaskId = UIBackgroundTaskInvalid;
|
||||
#endif
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithAdvertisedName:(NSString *)advertisedName
|
||||
restoreIdentifier:(NSString *)restoreIdentifier {
|
||||
return [self initWithAdvertisedName:advertisedName
|
||||
restoreIdentifier:restoreIdentifier
|
||||
queue:dispatch_get_main_queue()];
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[self stop];
|
||||
}
|
||||
|
||||
- (void)addPeripheralServiceManager:(GNSPeripheralServiceManager *)peripheralServiceManager
|
||||
bleServiceAddedCompletion:(GNSErrorHandler)completion {
|
||||
[_peripheralServiceManagers setObject:peripheralServiceManager
|
||||
forKey:peripheralServiceManager.serviceUUID];
|
||||
[peripheralServiceManager addedToPeripheralManager:self bleServiceAddedCompletion:completion];
|
||||
if (_started) {
|
||||
[self addBleServiceForServiceManager:peripheralServiceManager];
|
||||
}
|
||||
// Update all advertised services to make sure that the right services are advertised in case
|
||||
// all BLE services were already added.
|
||||
[self updateAdvertisedServices];
|
||||
}
|
||||
|
||||
- (void)start {
|
||||
if (_started) {
|
||||
GTMLoggerInfo(@"Peripheral manager already started.");
|
||||
return;
|
||||
}
|
||||
NSMutableDictionary<NSString *, id> *options =
|
||||
[@{CBCentralManagerOptionShowPowerAlertKey : @NO} mutableCopy];
|
||||
#if TARGET_OS_IPHONE
|
||||
// Restored API only supported on iOS.
|
||||
if (_restoreIdentifier) {
|
||||
options[CBPeripheralManagerOptionRestoreIdentifierKey] = _restoreIdentifier;
|
||||
}
|
||||
#endif
|
||||
_cbPeripheralManager = [self cbPeripheralManagerWithDelegate:self queue:_queue options:options];
|
||||
GTMLoggerInfo(@"Peripheral manager started.");
|
||||
_started = YES;
|
||||
// From Apple documentation |-[GNSPeripheralManager peripheralManagerDidUpdateState:]| will be
|
||||
// called no matter the current bluetooth state. Also, if the CBPeripheralManager is being
|
||||
// restored by iOS, |-[GNSPeripheralManager peripheralManager:willRestoreState:]| will be called
|
||||
// before. Therefore, adding and advertising services have to be done in
|
||||
// |peripheralManagerDidUpdateState:| method, so that the restored services will not be added
|
||||
// (if there are restored services).
|
||||
}
|
||||
|
||||
- (void)stop {
|
||||
if (!_started) {
|
||||
GTMLoggerInfo(@"Peripheral manager already stopped.");
|
||||
return;
|
||||
}
|
||||
GTMLoggerInfo(@"Peripheral manager stopped.");
|
||||
_started = NO;
|
||||
[self removeAllBleServicesAndStopAdvertising];
|
||||
_cbPeripheralManager.delegate = nil;
|
||||
_cbPeripheralManager = nil;
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
return [NSString stringWithFormat:@"<%@ %p: started %@, services %@>", self.class, self,
|
||||
_started ? @"YES" : @"NO",
|
||||
_peripheralServiceManagers.keyEnumerator];
|
||||
}
|
||||
|
||||
- (void)addBleServiceForServiceManager:(GNSPeripheralServiceManager *)peripheralServiceManager {
|
||||
if (peripheralServiceManager.cbServiceState != GNSBluetoothServiceStateNotAdded) {
|
||||
// Ignored if the peripheralServiceManager.cbService was already added.
|
||||
return;
|
||||
}
|
||||
[peripheralServiceManager willAddCBService];
|
||||
NSAssert(peripheralServiceManager.cbService, @"%@ had no cbService", peripheralServiceManager);
|
||||
[_cbPeripheralManager addService:peripheralServiceManager.cbService];
|
||||
}
|
||||
|
||||
- (void)addAllBleServicesAndStartAdvertising {
|
||||
// Do not attempt to add any services if the Bluetooth Daemon is in a crash loop.
|
||||
if ([self isBTCrashLoop]) {
|
||||
// http://b/28875581 Adding new service when Bluetooth is powered off may be the reason for
|
||||
// the Bluetooth Daemon crash loop. Avoid adding any BLE services here and wait for the next
|
||||
// state change of the |_cbPeripheralManager|.
|
||||
GTMLoggerError(@"Bluetooth Daemon crash loop detected crashing. Avoid adding BLE services.");
|
||||
return;
|
||||
}
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
if (_backgroundTaskId == UIBackgroundTaskInvalid) {
|
||||
GTMLoggerInfo(@"Start background task to add BLE services and start advertising.");
|
||||
_backgroundTaskId = [[UIApplication sharedApplication]
|
||||
beginBackgroundTaskWithName:@"Add BLE services and start advertising"
|
||||
expirationHandler:^{
|
||||
GTMLoggerError(@"Application was suspended before add BLE services and start "
|
||||
@"advertising finished.");
|
||||
[[UIApplication sharedApplication] endBackgroundTask:_backgroundTaskId];
|
||||
_backgroundTaskId = UIBackgroundTaskInvalid;
|
||||
}];
|
||||
}
|
||||
#endif
|
||||
|
||||
for (GNSPeripheralServiceManager *peripheralServiceManager in _peripheralServiceManagers
|
||||
.objectEnumerator) {
|
||||
[self addBleServiceForServiceManager:peripheralServiceManager];
|
||||
}
|
||||
// Update all advertised services to make sure that the right services are advertised in case all
|
||||
// BLE services were already added.
|
||||
[self updateAdvertisedServices];
|
||||
}
|
||||
|
||||
- (void)removeAllBleServicesAndStopAdvertising {
|
||||
[_cbPeripheralManager stopAdvertising];
|
||||
_advertisementInProgressData = nil;
|
||||
_advertisementData = nil;
|
||||
|
||||
// Remove services and inform all service managers that their service was removed.
|
||||
[_cbPeripheralManager removeAllServices];
|
||||
for (GNSPeripheralServiceManager *peripheralServiceManager in _peripheralServiceManagers
|
||||
.objectEnumerator) {
|
||||
[peripheralServiceManager didRemoveCBService];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateAdvertisedServices {
|
||||
if (!_started) {
|
||||
// Do not start advertising if this peripheral manager is stopped.
|
||||
return;
|
||||
}
|
||||
|
||||
CBManagerState cbState = _cbPeripheralManager.state;
|
||||
if ((cbState != CBManagerStatePoweredOn) && (cbState != CBManagerStatePoweredOff)) {
|
||||
GTMLoggerInfo(@"Do not start advertising on state %@", CBManagerStateString(cbState));
|
||||
return;
|
||||
}
|
||||
if (_advertisementInProgressData) {
|
||||
GTMLoggerInfo(@"Another start advertising operation is in progress.");
|
||||
return;
|
||||
}
|
||||
if (!_cbPeripheralManager.isAdvertising) {
|
||||
GTMLoggerInfo(@"Reset the advertiment data as the peripheral is not advertising.");
|
||||
_advertisementData = nil;
|
||||
}
|
||||
|
||||
BOOL allServicesAdded = YES;
|
||||
NSMutableArray<CBUUID *> *serviceUUIDsToAdvertise = [NSMutableArray array];
|
||||
for (GNSPeripheralServiceManager *peripheralServiceManager in _peripheralServiceManagers
|
||||
.objectEnumerator) {
|
||||
if (peripheralServiceManager.cbServiceState != GNSBluetoothServiceStateAdded) {
|
||||
allServicesAdded = NO;
|
||||
break;
|
||||
}
|
||||
if (peripheralServiceManager.isAdvertising) {
|
||||
[serviceUUIDsToAdvertise addObject:peripheralServiceManager.serviceUUID];
|
||||
}
|
||||
}
|
||||
if (!allServicesAdded) {
|
||||
GTMLoggerInfo(@"Do not start advertising as not all bluetooth services are added.");
|
||||
return;
|
||||
}
|
||||
NSMutableDictionary<NSString *, id> *advertisementData = [NSMutableDictionary dictionary];
|
||||
advertisementData[CBAdvertisementDataServiceUUIDsKey] = serviceUUIDsToAdvertise;
|
||||
if (_advertisedName) {
|
||||
advertisementData[CBAdvertisementDataLocalNameKey] = _advertisedName;
|
||||
}
|
||||
if ([advertisementData isEqual:_advertisementData]) {
|
||||
GTMLoggerInfo(
|
||||
@"Finished adding BLE services and starting advertising (advertisement data = %@).",
|
||||
_advertisementData);
|
||||
#if TARGET_OS_IPHONE
|
||||
[[UIApplication sharedApplication] endBackgroundTask:_backgroundTaskId];
|
||||
_backgroundTaskId = UIBackgroundTaskInvalid;
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
GTMLoggerInfo(@"Start advertising: %@", advertisementData);
|
||||
_advertisementInProgressData = [advertisementData copy];
|
||||
[_cbPeripheralManager stopAdvertising];
|
||||
[_cbPeripheralManager startAdvertising:_advertisementInProgressData];
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (void)updateOutgoingCharOnSocket:(GNSSocket *)socket withHandler:(GNSUpdateValueHandler)handler {
|
||||
NSUUID *socketIdentifier = socket.socketIdentifier;
|
||||
NSMutableArray<GNSUpdateValueHandler> *handlerQueue =
|
||||
_handlerQueuePerSocketIdentifier[socketIdentifier];
|
||||
if (!handlerQueue) {
|
||||
if (handler()) return;
|
||||
handlerQueue = [NSMutableArray array];
|
||||
_handlerQueuePerSocketIdentifier[socketIdentifier] = handlerQueue;
|
||||
GTMLoggerInfo(@"Queueing value for socket: %@", socketIdentifier);
|
||||
}
|
||||
[handlerQueue addObject:[handler copy]];
|
||||
}
|
||||
|
||||
- (void)processUpdateValueBlocks {
|
||||
GTMLoggerInfo(@"About to send values for sockets: %@", _handlerQueuePerSocketIdentifier.allKeys);
|
||||
for (NSUUID *socketIdentifier in _handlerQueuePerSocketIdentifier.allKeys) {
|
||||
NSMutableArray<GNSUpdateValueHandler> *handlerQueue =
|
||||
_handlerQueuePerSocketIdentifier[socketIdentifier];
|
||||
GTMLoggerInfo(@"%lu queued values to send.", (unsigned long)handlerQueue.count);
|
||||
while (handlerQueue.count > 0) {
|
||||
GNSUpdateValueHandler handler = handlerQueue[0];
|
||||
if (!handler()) {
|
||||
GTMLoggerInfo(@"Value failed to be send, still in the queue to try later.");
|
||||
break;
|
||||
}
|
||||
GTMLoggerInfo(@"Value sent successfully.");
|
||||
[handlerQueue removeObjectAtIndex:0];
|
||||
if (handlerQueue.count == 0) {
|
||||
GTMLoggerInfo(@"Removing pending value queue for socket: %@", socketIdentifier);
|
||||
[_handlerQueuePerSocketIdentifier removeObjectForKey:socketIdentifier];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)updateOutgoingCharacteristic:(NSData *)data onSocket:(GNSSocket *)socket {
|
||||
GNSPeripheralServiceManager *peripheralServiceManager = socket.owner;
|
||||
NSAssert(peripheralServiceManager, @"%@ should have an owner.", socket);
|
||||
return [_cbPeripheralManager updateValue:data
|
||||
forCharacteristic:peripheralServiceManager.weaveOutgoingCharacteristic
|
||||
onSubscribedCentrals:@[ socket.peerAsCentral ]];
|
||||
}
|
||||
|
||||
- (CBPeripheralManager *)cbPeripheralManagerWithDelegate:(id<CBPeripheralManagerDelegate>)delegate
|
||||
queue:(dispatch_queue_t)queue
|
||||
options:(NSDictionary<NSString *, id> *)options {
|
||||
return [[CBPeripheralManager alloc] initWithDelegate:delegate queue:queue options:options];
|
||||
}
|
||||
|
||||
- (void)socketDidDisconnect:(GNSSocket *)socket {
|
||||
NSAssert(!socket.connected, @"Should be disconnected");
|
||||
NSUUID *socketIdentifier = socket.socketIdentifier;
|
||||
NSMutableArray<GNSUpdateValueHandler> *handlerQueue =
|
||||
_handlerQueuePerSocketIdentifier[socketIdentifier];
|
||||
// The pending handlers have to be called so the associated send data completion blocks are
|
||||
// called with a disconnect error.
|
||||
for (GNSUpdateValueHandler handler in handlerQueue) {
|
||||
handler();
|
||||
}
|
||||
[_handlerQueuePerSocketIdentifier removeObjectForKey:socketIdentifier];
|
||||
}
|
||||
|
||||
#pragma mark - Bluetooth Daemon Crash Loop
|
||||
|
||||
- (BOOL)isBTCrashLoop {
|
||||
NSTimeInterval timeSinceLastResetting = -[_btCrashLastResettingDate timeIntervalSinceNow];
|
||||
return (timeSinceLastResetting <= gKBTCrashLoopMaxTimeBetweenResetting) &&
|
||||
(_btCrashCount > gKBTCrashLoopMinNumberOfResetting);
|
||||
}
|
||||
|
||||
- (void)updateBTCrashLoopHeuristic {
|
||||
NSAssert(_cbPeripheralManager.state == CBCentralManagerStateResetting, @"Unexpected CB state %@",
|
||||
CBManagerStateString(_cbPeripheralManager.state));
|
||||
NSDate *now = [NSDate date];
|
||||
if ([now timeIntervalSinceDate:_btCrashLastResettingDate] >
|
||||
gKBTCrashLoopMaxTimeBetweenResetting) {
|
||||
_btCrashCount = 0;
|
||||
}
|
||||
_btCrashLastResettingDate = now;
|
||||
_btCrashCount++;
|
||||
}
|
||||
|
||||
#pragma mark - CBPeripheralManagerDelegate
|
||||
|
||||
- (void)peripheralManager:(CBPeripheralManager *)peripheral
|
||||
willRestoreState:(NSDictionary<NSString *, id> *)dict {
|
||||
#if TARGET_OS_IPHONE
|
||||
// Restored API only supported on iOS.
|
||||
GTMLoggerInfo(@"Restore bluetooth services");
|
||||
_advertisementData = dict[CBPeripheralManagerRestoredStateAdvertisementDataKey];
|
||||
for (CBMutableService *service in dict[CBPeripheralManagerRestoredStateServicesKey]) {
|
||||
GNSPeripheralServiceManager *serviceManager =
|
||||
[_peripheralServiceManagers objectForKey:service.UUID];
|
||||
if (serviceManager) {
|
||||
[serviceManager restoredCBService:service];
|
||||
} else {
|
||||
GTMLoggerError(@"restoreFromCBService %@", service);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheralManager {
|
||||
NSAssert(peripheralManager == _cbPeripheralManager, @"Wrong peripheral manager.");
|
||||
GTMLoggerInfo(@"Peripheral manager state updated: %@",
|
||||
CBManagerStateString(_cbPeripheralManager.state));
|
||||
switch (_cbPeripheralManager.state) {
|
||||
case CBManagerStatePoweredOn:
|
||||
case CBManagerStatePoweredOff:
|
||||
// As instructed by Apple engineers:
|
||||
// * BLE services should be added on power or on power off.
|
||||
// * BLE advertisment should be started on power on.
|
||||
//
|
||||
// Note 1: During testing it seems safe to start advertising when Bluetooth is turned off:
|
||||
// the advertisment resumes when the state changes to power on.
|
||||
//
|
||||
// Note 2: It is important to keep the added services and to keep advertising on power off,
|
||||
// to avoid the scenario when the application receives a power off notification right before
|
||||
// being suspended by the OS.
|
||||
[self addAllBleServicesAndStartAdvertising];
|
||||
break;
|
||||
case CBManagerStateResetting:
|
||||
// As instructed by Apple enginners, clean-up all internal state when CoreBluetooth is
|
||||
// resetting.
|
||||
[self updateBTCrashLoopHeuristic];
|
||||
[self removeAllBleServicesAndStopAdvertising];
|
||||
break;
|
||||
case CBManagerStateUnknown:
|
||||
// Clean-up all internal state when the CoreBluetooth state is unknown.
|
||||
[self removeAllBleServicesAndStopAdvertising];
|
||||
break;
|
||||
case CBManagerStateUnauthorized:
|
||||
// Clean-up all internal state if the application is not authorized to use Bluetooth.
|
||||
[self removeAllBleServicesAndStopAdvertising];
|
||||
break;
|
||||
case CBManagerStateUnsupported:
|
||||
// The application should have never attempted to start advertising if Bluetooth Low Energy
|
||||
// is not supported on the device.
|
||||
NSAssert(!peripheralManager.isAdvertising, @"Peripheral is not advertising");
|
||||
break;
|
||||
}
|
||||
if (_peripheralManagerStateHandler) {
|
||||
_peripheralManagerStateHandler(_cbPeripheralManager.state);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral
|
||||
error:(NSError *)error {
|
||||
_advertisementData = _advertisementInProgressData;
|
||||
_advertisementInProgressData = nil;
|
||||
if (error) {
|
||||
GTMLoggerError(@"Start advertisment failed with error %@. Will retry to start advertising on "
|
||||
"the next BLE state change notification. ",
|
||||
error);
|
||||
_advertisementData = nil;
|
||||
return;
|
||||
}
|
||||
|
||||
NSAssert(peripheral.isAdvertising, @"Peripheral should be advertising.");
|
||||
GTMLoggerInfo(@"Peripheral did start advertising %@", _advertisementData);
|
||||
|
||||
// Once an advertisment operation is over, check if the advertised data is up-to-date.
|
||||
[self updateAdvertisedServices];
|
||||
}
|
||||
|
||||
- (void)peripheralManager:(CBPeripheralManager *)peripheral
|
||||
didAddService:(CBService *)service
|
||||
error:(NSError *)error {
|
||||
GNSPeripheralServiceManager *peripheralServiceManager =
|
||||
[_peripheralServiceManagers objectForKey:service.UUID];
|
||||
if (!peripheralServiceManager) {
|
||||
GTMLoggerError(@"Unknown service %@ with %@", service, self);
|
||||
return;
|
||||
}
|
||||
[peripheralServiceManager didAddCBServiceWithError:error];
|
||||
[self updateAdvertisedServices];
|
||||
}
|
||||
|
||||
- (void)peripheralManager:(CBPeripheralManager *)peripheral
|
||||
central:(CBCentral *)central
|
||||
didSubscribeToCharacteristic:(CBCharacteristic *)characteristic {
|
||||
GNSPeripheralServiceManager *peripheralServiceManager =
|
||||
[_peripheralServiceManagers objectForKey:characteristic.service.UUID];
|
||||
[peripheralServiceManager central:central didSubscribeToCharacteristic:characteristic];
|
||||
}
|
||||
|
||||
- (void)peripheralManager:(CBPeripheralManager *)peripheral
|
||||
central:(CBCentral *)central
|
||||
didUnsubscribeFromCharacteristic:(CBCharacteristic *)characteristic {
|
||||
GNSPeripheralServiceManager *peripheralServiceManager =
|
||||
[_peripheralServiceManagers objectForKey:characteristic.service.UUID];
|
||||
[peripheralServiceManager central:central didUnsubscribeFromCharacteristic:characteristic];
|
||||
|
||||
// Restarting the peripheral manager after a disconnect. This is a workaround for b/31752176.
|
||||
// Update: This problem persists in iOS 10 & 11. Discussion: https://goo.gl/fdE19G
|
||||
[self stop];
|
||||
[self start];
|
||||
}
|
||||
|
||||
- (void)peripheralManager:(CBPeripheralManager *)peripheral
|
||||
didReceiveReadRequest:(CBATTRequest *)request {
|
||||
GNSPeripheralServiceManager *peripheralServiceManager =
|
||||
[_peripheralServiceManagers objectForKey:request.characteristic.service.UUID];
|
||||
if (!peripheralServiceManager) {
|
||||
[_cbPeripheralManager respondToRequest:request withResult:CBATTErrorAttributeNotFound];
|
||||
return;
|
||||
}
|
||||
CBATTError requestError = [peripheralServiceManager canProcessReadRequest:request];
|
||||
if (requestError != CBATTErrorSuccess) {
|
||||
[_cbPeripheralManager respondToRequest:request withResult:requestError];
|
||||
return;
|
||||
}
|
||||
[peripheralServiceManager processReadRequest:request];
|
||||
[_cbPeripheralManager respondToRequest:request withResult:CBATTErrorSuccess];
|
||||
}
|
||||
|
||||
- (void)peripheralManager:(CBPeripheralManager *)peripheral
|
||||
didReceiveWriteRequests:(NSArray<CBATTRequest *> *)requests {
|
||||
// According to Apple's doc, if any one request cannot be processed, no request should
|
||||
// be processed. Only one CBATTError should be returned with the first request.
|
||||
if (requests.count == 0) {
|
||||
return;
|
||||
}
|
||||
CBATTError requestError = CBATTErrorSuccess;
|
||||
for (CBATTRequest *request in requests) {
|
||||
GNSPeripheralServiceManager *peripheralServiceManager =
|
||||
[_peripheralServiceManagers objectForKey:request.characteristic.service.UUID];
|
||||
if (!peripheralServiceManager) {
|
||||
requestError = CBATTErrorAttributeNotFound;
|
||||
break;
|
||||
}
|
||||
requestError = [peripheralServiceManager canProcessWriteRequest:request];
|
||||
if (requestError != CBATTErrorSuccess) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (requestError != CBATTErrorSuccess) {
|
||||
[_cbPeripheralManager respondToRequest:requests[0] withResult:requestError];
|
||||
return;
|
||||
}
|
||||
for (CBATTRequest *request in requests) {
|
||||
GNSPeripheralServiceManager *peripheralServiceManager =
|
||||
[_peripheralServiceManagers objectForKey:request.characteristic.service.UUID];
|
||||
[peripheralServiceManager processWriteRequest:request];
|
||||
}
|
||||
[_cbPeripheralManager respondToRequest:requests[0] withResult:requestError];
|
||||
}
|
||||
|
||||
- (void)peripheralManagerIsReadyToUpdateSubscribers:(CBPeripheralManager *)peripheral {
|
||||
GTMLoggerInfo(@"Ready to update subscribers.");
|
||||
[self processUpdateValueBlocks];
|
||||
}
|
||||
|
||||
@end
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralServiceManager.h"
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket+Private.h"
|
||||
|
||||
typedef NS_ENUM(NSInteger, GNSBluetoothServiceState) {
|
||||
GNSBluetoothServiceStateNotAdded,
|
||||
GNSBluetoothServiceStateAddInProgress,
|
||||
GNSBluetoothServiceStateAdded,
|
||||
};
|
||||
|
||||
/**
|
||||
* Private methods called by GNSPeripheralManager, GNSSocket and for tests.
|
||||
* Should not be used by the Nearby Socket client.
|
||||
*/
|
||||
@interface GNSPeripheralServiceManager ()<GNSSocketOwner>
|
||||
|
||||
@property(nonatomic, readonly) GNSPeripheralManager *peripheralManager;
|
||||
@property(nonatomic, readonly) GNSBluetoothServiceState cbServiceState;
|
||||
@property(nonatomic, readonly) CBMutableService *cbService;
|
||||
@property(nonatomic, readonly) CBMutableCharacteristic *weaveIncomingCharacteristic;
|
||||
@property(nonatomic, readonly) CBMutableCharacteristic *weaveOutgoingCharacteristic;
|
||||
@property(nonatomic, readonly) CBMutableCharacteristic *pairingCharacteristic;
|
||||
@property(nonatomic, readonly) GNSShouldAcceptSocketHandler shouldAcceptSocketHandler;
|
||||
|
||||
/**
|
||||
* Informs this service manager that its CBService will start to be added.
|
||||
*
|
||||
* Note that when the application is started in background on a Bluetooth event, the Bluetooth
|
||||
* services are restored in state GNSBluetoothServiceStateAdded and this method will never
|
||||
* called.
|
||||
*/
|
||||
- (void)willAddCBService;
|
||||
|
||||
/**
|
||||
* Informs this service manager that its CBService was added or failed to be added.
|
||||
*
|
||||
* Note that when the application is started in background on a Bluetooth event, the Bluetooth
|
||||
* services are restored in state GNSBluetoothServiceStateAdded and this method will never
|
||||
* called.
|
||||
*/
|
||||
- (void)didAddCBServiceWithError:(NSError *)error;
|
||||
|
||||
/**
|
||||
* Informs this service manager that its CBService was removed.
|
||||
*/
|
||||
- (void)didRemoveCBService;
|
||||
|
||||
/**
|
||||
* Receives the CBMutableService restored by iOS.
|
||||
*
|
||||
* @param service Restored service.
|
||||
*/
|
||||
- (void)restoredCBService:(CBMutableService *)service;
|
||||
|
||||
/**
|
||||
* Called only once in the instance life time. Called when the instance is attached to
|
||||
* a GNSPeripheralManager instance. Should assert when this method is called twice.
|
||||
*
|
||||
* @param peripheralManager GNSPeripheralManager
|
||||
* @param completion Callback when the BLE service is added.
|
||||
*/
|
||||
- (void)addedToPeripheralManager:(GNSPeripheralManager *)peripheralManager
|
||||
bleServiceAddedCompletion:(GNSErrorHandler)completion;
|
||||
|
||||
/**
|
||||
* Checks if a read request can be processed (based on the central and the characteristic).
|
||||
*
|
||||
* @param request Read request
|
||||
*
|
||||
* @return Error if cannot process the request
|
||||
*/
|
||||
- (CBATTError)canProcessReadRequest:(CBATTRequest *)request;
|
||||
|
||||
/**
|
||||
* Process a read request.
|
||||
*
|
||||
* @param request Read request
|
||||
*/
|
||||
- (void)processReadRequest:(CBATTRequest *)request;
|
||||
|
||||
/**
|
||||
* Checks if a write request can be processed (based on the central and the characteristic).
|
||||
*
|
||||
* @param request Write request
|
||||
*
|
||||
* @return Error if cannot process the request
|
||||
*/
|
||||
- (CBATTError)canProcessWriteRequest:(CBATTRequest *)request;
|
||||
|
||||
/**
|
||||
* Process the data according to the signal send in the data. See GNSControlSignal.
|
||||
*
|
||||
* @param request Request from CBCentral
|
||||
*/
|
||||
- (void)processWriteRequest:(CBATTRequest *)request;
|
||||
|
||||
/**
|
||||
* Called when a central subscribes to a characteristic. If the characteristic is the outgoing
|
||||
* characteristic, the desired connection latency is set to low.
|
||||
*
|
||||
* @param central The central subscribing to the characteristic
|
||||
* @param characteristic The characteristic being subscribe.
|
||||
*/
|
||||
- (void)central:(CBCentral *)central
|
||||
didSubscribeToCharacteristic:(CBCharacteristic *)characteristic;
|
||||
|
||||
/**
|
||||
* Called when a central unsubscribes from a characteristic. If the characteristic is the outgoing
|
||||
* characteristic and a socket still exists with this central, then the socket is disconnected
|
||||
* (as if received the disconnect signal).
|
||||
*
|
||||
* @param central The central unsubscribing to the characteristic
|
||||
* @param characteristic The characteristic being unsubscribed.
|
||||
*/
|
||||
- (void)central:(CBCentral *)central
|
||||
didUnsubscribeFromCharacteristic:(CBCharacteristic *)characteristic;
|
||||
|
||||
@end
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import <CoreBluetooth/CoreBluetooth.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class GNSPeripheralManager;
|
||||
@class GNSPeripheralServiceManager;
|
||||
@class GNSSocket;
|
||||
|
||||
typedef BOOL (^GNSShouldAcceptSocketHandler)(GNSSocket *socket);
|
||||
|
||||
/**
|
||||
* This class manages one BLE service. It keeps track of the list of centrals connected with
|
||||
* a socket to this service. This class has also the knowledge to parse the |GNSControlSignal|.
|
||||
* One instance is created per BLE service and each instance can be used only once.
|
||||
*
|
||||
* This class is not thread-safe.
|
||||
*/
|
||||
@interface GNSPeripheralServiceManager : NSObject
|
||||
|
||||
/**
|
||||
* Core Bluetooth service UUID.
|
||||
*/
|
||||
@property(nonatomic, readonly) CBUUID *serviceUUID;
|
||||
|
||||
/**
|
||||
* Makes this service being advertised by Core Bluetooth or not. Even if the service is not
|
||||
* advertised, the service is still present in the service database, and thus can still be used
|
||||
* to send/receive data. The default value is YES.
|
||||
*/
|
||||
@property(nonatomic, getter=isAdvertising) BOOL advertising;
|
||||
|
||||
/**
|
||||
* Creates an instance to manage a BLE service. The BLE service can be used to create one socket
|
||||
* per central. When a central tries to connected, |shouldAcceptSocketHandler| is called with the
|
||||
* socket to the central. If the handler returns YES, the socket is accepted. The socket delegate
|
||||
* should be set before returning from |shouldAcceptSocketHandler| (if the socket is accepted).
|
||||
* The socket will be ready to use shortly after.
|
||||
* -[id<GNSSocketDelegate> socketDidConnect:] will be called once the socket
|
||||
* is ready to send and receive data. The socket is retained by GNSPeripheralServiceManager as
|
||||
* long as it is connected.
|
||||
*
|
||||
* @param serviceUUID Service UUID
|
||||
* @param addPairingCharacteristic If YES, will advertise an encrypted characteristic used by
|
||||
* the central if it wants to do a bluetooth pairing
|
||||
* @param shouldAcceptSocketHandler Handler called when a central starts a new socket. The delegate
|
||||
* socket can be set if the socket is accepted.
|
||||
* @param queue The queue this object is called on and callbacks are made on.
|
||||
*
|
||||
* @return GNSPeripheralServiceManager instance
|
||||
*/
|
||||
- (instancetype)initWithBleServiceUUID:(CBUUID *)serviceUUID
|
||||
addPairingCharacteristic:(BOOL)addPairingCharacteristic
|
||||
shouldAcceptSocketHandler:(GNSShouldAcceptSocketHandler)shouldAcceptSocketHandler
|
||||
queue:(dispatch_queue_t)queue
|
||||
NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
/**
|
||||
* Creates an instance using the main queue for callbacks.
|
||||
*
|
||||
* @return GNSPeripheralServiceManager instance
|
||||
*/
|
||||
- (instancetype)initWithBleServiceUUID:(CBUUID *)serviceUUID
|
||||
addPairingCharacteristic:(BOOL)addPairingCharacteristic
|
||||
shouldAcceptSocketHandler:(GNSShouldAcceptSocketHandler)shouldAcceptSocketHandler;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
@end
|
||||
+551
@@ -0,0 +1,551 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralServiceManager+Private.h"
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Peripheral/GNSPeripheralManager+Private.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket+Private.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSUtils.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSWeavePacket.h"
|
||||
#import "GoogleToolboxForMac/GTMLogger.h"
|
||||
|
||||
// The Weave BLE protocol has only one valid version.
|
||||
static const UInt16 kWeaveVersionSupported = 1;
|
||||
|
||||
static NSString *GetBluetoothServiceStateDescription(GNSBluetoothServiceState state) {
|
||||
switch (state) {
|
||||
case GNSBluetoothServiceStateNotAdded:
|
||||
return @"NotAdded";
|
||||
case GNSBluetoothServiceStateAddInProgress:
|
||||
return @"AddInProgress";
|
||||
case GNSBluetoothServiceStateAdded:
|
||||
return @"Added";
|
||||
}
|
||||
NSCAssert(NO, @"Wrong GNSBluetoothServiceState %ld", (long)state);
|
||||
return @"";
|
||||
}
|
||||
|
||||
@interface GNSPeripheralServiceManager ()<GNSWeavePacketHandler> {
|
||||
GNSPeripheralManager *_peripheralManager;
|
||||
BOOL _addPairingCharacteristic;
|
||||
CBMutableService *_cbService;
|
||||
|
||||
// Weave characteristics UUIDs are different from legagy ones.
|
||||
CBMutableCharacteristic *_weaveIncomingChar;
|
||||
CBMutableCharacteristic *_weaveOutgoingChar;
|
||||
|
||||
// Pairing chacteristic is used by both Weave and legacy protocols. Changing the UUID of this
|
||||
// characteristic has no impact in both Weave and legacy protocols (CrOS doesn't need it).
|
||||
CBMutableCharacteristic *_pairingChar;
|
||||
|
||||
// key: central identifier, value: GNSPeripheral
|
||||
NSMutableDictionary<NSUUID *, GNSSocket *> *_sockets;
|
||||
GNSShouldAcceptSocketHandler _shouldAcceptSocketHandler;
|
||||
GNSErrorHandler _bleServiceAddedCompletion;
|
||||
|
||||
dispatch_queue_t _queue;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
static CBMutableCharacteristic *CreateWeaveIncomingCharacteristic() {
|
||||
return [[CBMutableCharacteristic alloc]
|
||||
initWithType:[CBUUID UUIDWithString:kGNSWeaveToPeripheralCharUUIDString]
|
||||
properties:CBCharacteristicPropertyWrite
|
||||
value:nil
|
||||
permissions:CBAttributePermissionsWriteable];
|
||||
}
|
||||
|
||||
static CBMutableCharacteristic *CreateWeaveOutgoingCharacteristic() {
|
||||
return [[CBMutableCharacteristic alloc]
|
||||
initWithType:[CBUUID UUIDWithString:kGNSWeaveFromPeripheralCharUUIDString]
|
||||
properties:CBCharacteristicPropertyIndicate
|
||||
value:nil
|
||||
permissions:CBAttributePermissionsReadable];
|
||||
}
|
||||
|
||||
static CBMutableCharacteristic *CreatePairingCharacteristic() {
|
||||
return [[CBMutableCharacteristic alloc]
|
||||
initWithType:[CBUUID UUIDWithString:kGNSPairingCharUUIDString]
|
||||
properties:CBCharacteristicPropertyRead
|
||||
value:nil
|
||||
permissions:CBAttributePermissionsReadEncryptionRequired];
|
||||
}
|
||||
|
||||
@implementation GNSPeripheralServiceManager
|
||||
|
||||
- (instancetype)init {
|
||||
[self doesNotRecognizeSelector:_cmd];
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (instancetype)initWithBleServiceUUID:(CBUUID *)serviceUUID
|
||||
addPairingCharacteristic:(BOOL)addPairingCharacteristic
|
||||
shouldAcceptSocketHandler:(GNSShouldAcceptSocketHandler)shouldAcceptSocketHandler
|
||||
queue:(dispatch_queue_t)queue {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
NSParameterAssert(shouldAcceptSocketHandler);
|
||||
_advertising = YES;
|
||||
_serviceUUID = serviceUUID;
|
||||
_addPairingCharacteristic = addPairingCharacteristic;
|
||||
_shouldAcceptSocketHandler = [shouldAcceptSocketHandler copy];
|
||||
_sockets = [NSMutableDictionary dictionary];
|
||||
_queue = queue;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithBleServiceUUID:(CBUUID *)serviceUUID
|
||||
addPairingCharacteristic:(BOOL)addPairingCharacteristic
|
||||
shouldAcceptSocketHandler:(GNSShouldAcceptSocketHandler)shouldAcceptSocketHandler {
|
||||
return [self initWithBleServiceUUID:serviceUUID
|
||||
addPairingCharacteristic:addPairingCharacteristic
|
||||
shouldAcceptSocketHandler:shouldAcceptSocketHandler
|
||||
queue:dispatch_get_main_queue()];
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
return [NSString
|
||||
stringWithFormat:
|
||||
@"<%@: %p, %@, CBService state: %@, serviceUUID: %@, add pairing char: %@, sockets: %ld>",
|
||||
[self class], self, _advertising ? @"advertising" : @"not advertising",
|
||||
GetBluetoothServiceStateDescription(_cbServiceState), _serviceUUID,
|
||||
_addPairingCharacteristic ? @"YES" : @"NO", (unsigned long)_sockets.count];
|
||||
}
|
||||
|
||||
- (void)willAddCBService {
|
||||
NSAssert(_cbServiceState == GNSBluetoothServiceStateNotAdded, @"Unexpected CBService state %@ %@",
|
||||
GetBluetoothServiceStateDescription(_cbServiceState), self);
|
||||
NSAssert(_cbService == nil, @"Service should not exist before it is actually added %@", self);
|
||||
|
||||
_cbServiceState = GNSBluetoothServiceStateAddInProgress;
|
||||
NSAssert(!_weaveIncomingChar, @"Should have no weave incoming characteristic %@.", self);
|
||||
_weaveIncomingChar = CreateWeaveIncomingCharacteristic();
|
||||
NSAssert(!_weaveOutgoingChar, @"Should have no weave outgoing characteristic %@.", self);
|
||||
_weaveOutgoingChar = CreateWeaveOutgoingCharacteristic();
|
||||
NSAssert(!_pairingChar, @"Should have no pairing characteristic %@.", self);
|
||||
if (_addPairingCharacteristic) {
|
||||
_pairingChar = CreatePairingCharacteristic();
|
||||
}
|
||||
_cbService = [[CBMutableService alloc] initWithType:_serviceUUID primary:YES];
|
||||
if (_pairingChar) {
|
||||
_cbService.characteristics = @[ _weaveIncomingChar, _weaveOutgoingChar, _pairingChar ];
|
||||
} else {
|
||||
_cbService.characteristics = @[ _weaveIncomingChar, _weaveOutgoingChar ];
|
||||
}
|
||||
GTMLoggerInfo(@"Will add BLE service: %@", _cbService);
|
||||
}
|
||||
|
||||
- (void)didAddCBServiceWithError:(NSError *)error {
|
||||
if (_cbServiceState == GNSBluetoothServiceStateNotAdded) {
|
||||
// Ignored as the service was probably removed while the add CBService operation was in
|
||||
// progress.
|
||||
GTMLoggerError(@"Ignore adding BLE service %@ result (error = %@) %@", _cbService, error, self);
|
||||
return;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
GTMLoggerError(@"Failed adding BLE service %@ (error = %@) %@", _cbService, error, self);
|
||||
[self didRemoveCBService];
|
||||
} else {
|
||||
GTMLoggerInfo(@"Finished adding BLE service %@", _cbService);
|
||||
_cbServiceState = GNSBluetoothServiceStateAdded;
|
||||
}
|
||||
if (_bleServiceAddedCompletion) {
|
||||
_bleServiceAddedCompletion(error);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)didRemoveCBService {
|
||||
_cbServiceState = GNSBluetoothServiceStateNotAdded;
|
||||
_cbService = nil;
|
||||
_weaveIncomingChar = nil;
|
||||
_weaveOutgoingChar = nil;
|
||||
_pairingChar = nil;
|
||||
}
|
||||
|
||||
- (void)restoredCBService:(CBMutableService *)service {
|
||||
if (![service.UUID isEqual:_serviceUUID]) {
|
||||
GTMLoggerError(@"Cannot restore from bluetooth service %@.", service);
|
||||
return;
|
||||
}
|
||||
for (CBMutableCharacteristic *characteristic in service.characteristics) {
|
||||
if (_addPairingCharacteristic &&
|
||||
[characteristic.UUID isEqual:[CBUUID UUIDWithString:kGNSPairingCharUUIDString]]) {
|
||||
_pairingChar = characteristic;
|
||||
} else if ([characteristic.UUID
|
||||
isEqual:[CBUUID UUIDWithString:kGNSWeaveToPeripheralCharUUIDString]]) {
|
||||
_weaveIncomingChar = characteristic;
|
||||
} else if ([characteristic.UUID
|
||||
isEqual:[CBUUID UUIDWithString:kGNSWeaveFromPeripheralCharUUIDString]]) {
|
||||
_weaveOutgoingChar = characteristic;
|
||||
}
|
||||
}
|
||||
// TODO(jlebel): Need to find out how the service can be restored if a characteritic is missing.
|
||||
// b/25561363
|
||||
NSAssert(_weaveIncomingChar, @"Weave incoming characteristic missing");
|
||||
NSAssert(_weaveOutgoingChar, @"Weave outgoing characteristic missing");
|
||||
NSAssert(_pairingChar || !_addPairingCharacteristic, @"Pairing characteristic missing");
|
||||
_cbService = service;
|
||||
_cbServiceState = GNSBluetoothServiceStateAdded;
|
||||
GTMLoggerInfo(@"Service restored: %@", _cbService);
|
||||
}
|
||||
|
||||
- (void)setAdvertising:(BOOL)advertising {
|
||||
if (advertising == _advertising) {
|
||||
return;
|
||||
}
|
||||
_advertising = advertising;
|
||||
[_peripheralManager updateAdvertisedServices];
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (GNSShouldAcceptSocketHandler)shouldAcceptSocketHandler {
|
||||
return _shouldAcceptSocketHandler;
|
||||
}
|
||||
|
||||
- (GNSPeripheralManager *)peripheralManager {
|
||||
return _peripheralManager;
|
||||
}
|
||||
|
||||
- (CBMutableService *)cbService {
|
||||
return _cbService;
|
||||
}
|
||||
|
||||
- (CBMutableCharacteristic *)weaveIncomingCharacteristic {
|
||||
return _weaveIncomingChar;
|
||||
}
|
||||
|
||||
- (CBMutableCharacteristic *)weaveOutgoingCharacteristic {
|
||||
return _weaveOutgoingChar;
|
||||
}
|
||||
|
||||
- (CBMutableCharacteristic *)pairingCharacteristic {
|
||||
return _pairingChar;
|
||||
}
|
||||
|
||||
- (GNSErrorHandler)bleServiceAddedCompletion {
|
||||
return _bleServiceAddedCompletion;
|
||||
}
|
||||
|
||||
- (void)addedToPeripheralManager:(GNSPeripheralManager *)peripheralManager
|
||||
bleServiceAddedCompletion:(GNSErrorHandler)completion {
|
||||
NSAssert(_peripheralManager == nil,
|
||||
@"Do not reuse. Already associated with another peripheral manager %@",
|
||||
_peripheralManager);
|
||||
_peripheralManager = peripheralManager;
|
||||
_bleServiceAddedCompletion = [completion copy];
|
||||
}
|
||||
|
||||
- (CBATTError)canProcessReadRequest:(CBATTRequest *)request {
|
||||
if ([request.characteristic.UUID isEqual:_weaveOutgoingChar.UUID] ||
|
||||
[request.characteristic.UUID isEqual:_weaveIncomingChar.UUID]) {
|
||||
return CBATTErrorReadNotPermitted;
|
||||
}
|
||||
if (_pairingChar && [request.characteristic.UUID isEqual:_pairingChar.UUID]) {
|
||||
return CBATTErrorSuccess;
|
||||
}
|
||||
return CBATTErrorAttributeNotFound;
|
||||
}
|
||||
|
||||
- (void)processReadRequest:(CBATTRequest *)request {
|
||||
if (!_pairingChar || ![request.characteristic.UUID isEqual:_pairingChar.UUID]) {
|
||||
NSAssert(NO, @"Cannot read characteristic %@", request);
|
||||
return;
|
||||
}
|
||||
request.value = [NSData data];
|
||||
}
|
||||
|
||||
- (CBATTError)canProcessWriteRequest:(CBATTRequest *)request {
|
||||
if ([request.characteristic.UUID isEqual:_weaveIncomingChar.UUID]) {
|
||||
return CBATTErrorSuccess;
|
||||
} else if ([request.characteristic.UUID isEqual:_weaveOutgoingChar.UUID]) {
|
||||
return CBATTErrorWriteNotPermitted;
|
||||
}
|
||||
return CBATTErrorAttributeNotFound;
|
||||
}
|
||||
|
||||
- (void)handleWeaveError:(GNSError)errorCode socket:(GNSSocket *)socket {
|
||||
if (!socket) {
|
||||
return;
|
||||
}
|
||||
// Only send the error if no error packet was previously received.
|
||||
if (errorCode != GNSErrorWeaveErrorPacketReceived) {
|
||||
GTMLoggerInfo(@"Sending error packet for socket: %@", socket);
|
||||
GNSWeaveErrorPacket *errorPacket =
|
||||
[[GNSWeaveErrorPacket alloc] initWithPacketCounter:socket.sendPacketCounter];
|
||||
[self sendPacket:errorPacket toSocket:socket completion:^{
|
||||
GTMLoggerInfo(@"Error packet sent to socket: %@", socket);
|
||||
}];
|
||||
}
|
||||
NSError *error = GNSErrorWithCode(errorCode);
|
||||
[self removeSocket:socket withError:error];
|
||||
}
|
||||
|
||||
- (void)processWeaveWriteRequest:(CBATTRequest *)request {
|
||||
if (![request.characteristic.UUID isEqual:_weaveIncomingChar.UUID]) {
|
||||
GTMLoggerError(@"Cannot process %@ on characteristic %@ (%@)", request,
|
||||
request.characteristic.UUID,
|
||||
GNSCharacteristicName(request.characteristic.UUID.UUIDString));
|
||||
return;
|
||||
}
|
||||
GNSSocket *socket = _sockets[request.central.identifier];
|
||||
// We truncate the packet to size we are expecting: kGNSMinSupportedPacketSize (for the first
|
||||
// packet) and |socket.packetSize| (for the rest).
|
||||
NSUInteger truncatedSize =
|
||||
MIN(socket ? socket.packetSize : kGNSMinSupportedPacketSize, request.value.length);
|
||||
if (truncatedSize != request.value.length) {
|
||||
GTMLoggerInfo(@"Packet with %ld bytes truncated to %ld.", (long)request.value.length,
|
||||
(long)truncatedSize);
|
||||
}
|
||||
NSData *packetData = [request.value subdataWithRange:NSMakeRange(0, truncatedSize)];
|
||||
NSError *parsingError = nil;
|
||||
GNSWeavePacket *packet = [GNSWeavePacket parseData:packetData error:&parsingError];
|
||||
if (!packet) {
|
||||
GTMLoggerError(@"Error parsing weave packet (error = %@).", parsingError);
|
||||
[self handleWeaveError:GNSErrorParsingWeavePacket socket:socket];
|
||||
return;
|
||||
}
|
||||
if (!socket && ![packet isKindOfClass:[GNSWeaveConnectionRequestPacket class]]) {
|
||||
GTMLoggerInfo(@"Non-request weave packet received when no socket exists -- ignoring");
|
||||
return;
|
||||
}
|
||||
UInt8 expectedCounter = [packet isKindOfClass:[GNSWeaveConnectionRequestPacket class]] ?
|
||||
0 : socket.receivePacketCounter;
|
||||
if (packet.packetCounter != expectedCounter) {
|
||||
GTMLoggerError(@"Wrong packet counter, [received %d, expected %d].", packet.packetCounter,
|
||||
expectedCounter);
|
||||
[self handleWeaveError:GNSErrorWrongWeavePacketCounter socket:socket];
|
||||
return;
|
||||
}
|
||||
[packet visitWithHandler:self context:request];
|
||||
// A new socket was created if |request| contains a connection request packet. So we need to
|
||||
// update |socket| before incrementing the packet counter.
|
||||
socket = _sockets[request.central.identifier];
|
||||
|
||||
// Only an error packet can cause the socket to have been removed at this point.
|
||||
NSAssert(socket || [packet isKindOfClass:[GNSWeaveErrorPacket class]],
|
||||
@"Socket missing after receiving non-error weave packet");
|
||||
[socket incrementReceivePacketCounter];
|
||||
}
|
||||
|
||||
- (void)processWriteRequest:(CBATTRequest *)request {
|
||||
if (![request.characteristic.UUID isEqual:_weaveIncomingChar.UUID]) {
|
||||
GTMLoggerError(@"Cannot process %@ on characteristic %@ (%@)", request,
|
||||
request.characteristic.UUID,
|
||||
GNSCharacteristicName(request.characteristic.UUID.UUIDString));
|
||||
return;
|
||||
}
|
||||
GTMLoggerInfo(@"Write request on Weave characteristic.");
|
||||
[self processWeaveWriteRequest:request];
|
||||
}
|
||||
|
||||
// This method sends |packet| fitting a single characteristic write to |socket|. All packets send by
|
||||
// this class (not the socket) must use this method.
|
||||
- (void)sendPacket:(GNSWeavePacket *)packet
|
||||
toSocket:(GNSSocket *)socket
|
||||
completion:(void (^)(void))completion {
|
||||
NSAssert(packet.packetCounter == socket.sendPacketCounter, @"Wrong packet counter.");
|
||||
[socket incrementSendPacketCounter];
|
||||
// Do not check if the socket before retrying to send the packet if it's a connection confirm
|
||||
// packet, as the socket is only connected after that packet is sent.
|
||||
BOOL checkConnected = ![packet isKindOfClass:[GNSWeaveConnectionConfirmPacket class]];
|
||||
NSData *data = [packet serialize];
|
||||
[self sendData:data toSocket:socket checkConnected:checkConnected completion:completion];
|
||||
}
|
||||
|
||||
- (void)sendData:(NSData *)data
|
||||
toSocket:(GNSSocket *)socket
|
||||
checkConnected:(BOOL)checkConnected
|
||||
completion:(void (^)(void))completion {
|
||||
__weak __typeof__(self) weakSelf = self;
|
||||
[_peripheralManager
|
||||
updateOutgoingCharOnSocket:socket
|
||||
withHandler:^() {
|
||||
__typeof__(weakSelf) strongSelf = weakSelf;
|
||||
if (!strongSelf || (checkConnected && !socket.isConnected)) {
|
||||
// Socket is gone or disconnected; don't reschedule.
|
||||
return YES;
|
||||
}
|
||||
if (![strongSelf.peripheralManager updateOutgoingCharacteristic:data
|
||||
onSocket:socket]) {
|
||||
GTMLoggerInfo(@"Failed to update characteristic value; reschedule");
|
||||
return NO;
|
||||
}
|
||||
if (completion) {
|
||||
dispatch_async(_queue, ^{ completion(); });
|
||||
}
|
||||
return YES;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)socketReady:(GNSSocket *)socket {
|
||||
[socket didConnect];
|
||||
}
|
||||
|
||||
- (void)removeSocket:(GNSSocket *)socket withError:(NSError *)error {
|
||||
if (error) {
|
||||
GTMLoggerInfo(@"Socket disconnected with error, socket: %@, error: %@", socket, error);
|
||||
} else {
|
||||
GTMLoggerInfo(@"Socket disconnected %@", socket);
|
||||
}
|
||||
[_sockets removeObjectForKey:socket.peerIdentifier];
|
||||
[socket didDisconnectWithError:error];
|
||||
[_peripheralManager socketDidDisconnect:socket];
|
||||
}
|
||||
|
||||
- (void)central:(CBCentral *)central
|
||||
didSubscribeToCharacteristic:(CBCharacteristic *)characteristic {
|
||||
if ([characteristic.UUID isEqual:_weaveOutgoingChar.UUID]) {
|
||||
// TODO(sacomoto): The latency should be changed only with CrOS. When iOS is talking with OS X,
|
||||
// changing the latency increases the authentication time. See b/23719877.
|
||||
[_peripheralManager.cbPeripheralManager
|
||||
setDesiredConnectionLatency:CBPeripheralManagerConnectionLatencyLow
|
||||
forCentral:central];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)central:(CBCentral *)central
|
||||
didUnsubscribeFromCharacteristic:(CBCharacteristic *)characteristic {
|
||||
GNSSocket *socket = _sockets[central.identifier];
|
||||
if (socket && [characteristic.UUID isEqual:_weaveOutgoingChar.UUID]) {
|
||||
// Disconnect signal is optional. If the central unsubscribe to _outgoingChar, the socket
|
||||
// is disconnected.
|
||||
GTMLoggerInfo(@"%@ unsubscribe to outgoing characteristic", central);
|
||||
[self removeSocket:socket withError:nil];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - GNSSocketOwner
|
||||
|
||||
- (NSUInteger)socketMaximumUpdateValueLength:(GNSSocket *)socket {
|
||||
return [socket.peerAsCentral maximumUpdateValueLength];
|
||||
}
|
||||
|
||||
- (void)sendData:(NSData *)data socket:(GNSSocket *)socket completion:(GNSErrorHandler)completion {
|
||||
GTMLoggerInfo(@"Updating value in characteristic");
|
||||
[_peripheralManager updateOutgoingCharOnSocket:socket withHandler:^{
|
||||
BOOL wasSent = [_peripheralManager updateOutgoingCharacteristic:data onSocket:socket];
|
||||
if (wasSent) {
|
||||
GTMLoggerInfo(@"Successfully updated value in characteristic");
|
||||
dispatch_async(_queue, ^{ completion(nil); });
|
||||
} else {
|
||||
GTMLoggerInfo(@"Failed to update characteristic value; reschedule");
|
||||
}
|
||||
return wasSent;
|
||||
}];
|
||||
}
|
||||
|
||||
- (NSUUID *)socketServiceIdentifier:(GNSSocket *)socket {
|
||||
return [[NSUUID alloc] initWithUUIDString:_serviceUUID.UUIDString];
|
||||
}
|
||||
|
||||
- (void)disconnectSocket:(GNSSocket *)socket {
|
||||
if (!socket.isConnected) {
|
||||
return;
|
||||
}
|
||||
// The Weave protocol has no disconnect signal. As a temporary solution, we send an error
|
||||
// packet, as this will cause the central to disconnect.
|
||||
GNSWeaveErrorPacket *errorPacket =
|
||||
[[GNSWeaveErrorPacket alloc] initWithPacketCounter:socket.sendPacketCounter];
|
||||
__weak __typeof__(self) weakSelf = self;
|
||||
[self sendPacket:errorPacket
|
||||
toSocket:socket
|
||||
completion:^{
|
||||
if (weakSelf) {
|
||||
GTMLoggerInfo(@"Error packet sent (to force the central to disconnect).");
|
||||
[weakSelf removeSocket:socket withError:nil];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)socketWillBeDeallocated:(GNSSocket *)socket {
|
||||
}
|
||||
|
||||
#pragma mark - GNSWeavePacketHandler
|
||||
|
||||
- (void)handleConnectionRequestPacket:(GNSWeaveConnectionRequestPacket *)packet
|
||||
context:(id)request {
|
||||
NSAssert([request isKindOfClass:[CBATTRequest class]], @"The context should be a request.");
|
||||
GNSSocket *socket = _sockets[((CBATTRequest *)request).central.identifier];
|
||||
if (socket) {
|
||||
GTMLoggerInfo(@"Receiving a connection request from an already connected socket %@.", socket);
|
||||
// The peripheral considers the previous socket as being disconnected.
|
||||
NSError *error = GNSErrorWithCode(GNSErrorNewInviteToConnectReceived);
|
||||
[self removeSocket:socket withError:error];
|
||||
socket = nil;
|
||||
}
|
||||
socket = [[GNSSocket alloc] initWithOwner:self
|
||||
centralPeer:((CBATTRequest *)request).central
|
||||
queue:_queue];
|
||||
if (packet.maxVersion < kWeaveVersionSupported || packet.minVersion > kWeaveVersionSupported) {
|
||||
GTMLoggerError(@"Unsupported Weave version range: [%d, %d].", packet.minVersion,
|
||||
packet.maxVersion);
|
||||
[self handleWeaveError:GNSErrorUnsupportedWeaveProtocolVersion socket:socket];
|
||||
return;
|
||||
}
|
||||
if (packet.maxPacketSize == 0) {
|
||||
socket.packetSize = [self socketMaximumUpdateValueLength:socket];
|
||||
} else {
|
||||
socket.packetSize = MIN(packet.maxPacketSize, [self socketMaximumUpdateValueLength:socket]);
|
||||
}
|
||||
if (_shouldAcceptSocketHandler(socket)) {
|
||||
_sockets[socket.peerIdentifier] = socket;
|
||||
|
||||
dispatch_async(_queue, ^{
|
||||
GTMLoggerInfo(@"Sending connection confirm packet.");
|
||||
GNSWeaveConnectionConfirmPacket *confirm =
|
||||
[[GNSWeaveConnectionConfirmPacket alloc] initWithVersion:kWeaveVersionSupported
|
||||
packetSize:socket.packetSize
|
||||
data:nil];
|
||||
__weak __typeof__(self) weakSelf = self;
|
||||
[self sendPacket:confirm toSocket:socket completion:^{
|
||||
if (weakSelf) {
|
||||
GTMLoggerInfo(@"Connection confirm packet sent.");
|
||||
[weakSelf socketReady:socket];
|
||||
}
|
||||
}];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleConnectionConfirmPacket:(GNSWeaveConnectionConfirmPacket *)packet
|
||||
context:(id)request {
|
||||
NSAssert([request isKindOfClass:[CBATTRequest class]], @"The context should be a request.");
|
||||
GNSSocket *socket = _sockets[((CBATTRequest *)request).central.identifier];
|
||||
GTMLoggerError(@"Unexpected connection confirm packet received.");
|
||||
[self handleWeaveError:GNSErrorUnexpectedWeaveControlPacket socket:socket];
|
||||
}
|
||||
|
||||
- (void)handleErrorPacket:(GNSWeaveErrorPacket *)packet context:(id)request {
|
||||
NSAssert([request isKindOfClass:[CBATTRequest class]], @"The context should be a request.");
|
||||
GNSSocket *socket = _sockets[((CBATTRequest *)request).central.identifier];
|
||||
GTMLoggerInfo(@"Error packet received.");
|
||||
[self handleWeaveError:GNSErrorWeaveErrorPacketReceived socket:socket];
|
||||
}
|
||||
|
||||
- (void)handleDataPacket:(GNSWeaveDataPacket *)packet context:(id)request {
|
||||
NSAssert([request isKindOfClass:[CBATTRequest class]], @"The context should be a request.");
|
||||
GNSSocket *socket = _sockets[((CBATTRequest *)request).central.identifier];
|
||||
if (packet.isFirstPacket && socket.waitingForIncomingData) {
|
||||
GTMLoggerError(@"There is already a receive operation in progress");
|
||||
[self handleWeaveError:GNSErrorWeaveDataTransferInProgress socket:socket];
|
||||
return;
|
||||
}
|
||||
[socket didReceiveIncomingWeaveDataPacket:packet];
|
||||
}
|
||||
|
||||
@end
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket.h"
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSUtils+Private.h"
|
||||
|
||||
@class GNSWeaveDataPacket;
|
||||
|
||||
/**
|
||||
* Protocol implemented by the class who is in charge to create the socket. The owner knows how
|
||||
* send and receive data through Bluetooth BLE.
|
||||
*/
|
||||
@protocol GNSSocketOwner<NSObject>
|
||||
|
||||
/**
|
||||
* Returns the current MTU. Called by the socket when it needs to create chunks to send data.
|
||||
*
|
||||
* @param socket Current socket
|
||||
*
|
||||
* @return MTU.
|
||||
*/
|
||||
- (NSUInteger)socketMaximumUpdateValueLength:(GNSSocket *)socket;
|
||||
|
||||
/**
|
||||
* Called by the socket when it needs to send a chunk of data.
|
||||
*
|
||||
* @param data Chunk to send
|
||||
* @param completion Callback called when the data has been fully sent or an error has occurred.
|
||||
*/
|
||||
- (void)sendData:(NSData *)data socket:(GNSSocket *)socket completion:(GNSErrorHandler)completion;
|
||||
|
||||
/**
|
||||
* Returns the UUID of the peer.
|
||||
*
|
||||
* @param socket Current socket
|
||||
*
|
||||
* @return UUID based on the peer.
|
||||
*/
|
||||
- (NSUUID *)socketServiceIdentifier:(GNSSocket *)socket;
|
||||
|
||||
/**
|
||||
* Called by the socket when the socket should be disconnected.
|
||||
*
|
||||
* @param socket Current socket
|
||||
*/
|
||||
- (void)disconnectSocket:(GNSSocket *)socket;
|
||||
|
||||
/**
|
||||
* Called when the socket is deallocated. Some owners (like GNSCentralPeerManager) doesn't retain
|
||||
* the socket and let the socket user taking care of the retain/release. When this method is called
|
||||
* the socket should not be called anymore.
|
||||
*
|
||||
* @param socket Socket being deallocated.
|
||||
*/
|
||||
- (void)socketWillBeDeallocated:(GNSSocket *)socket;
|
||||
|
||||
@end
|
||||
|
||||
/**
|
||||
* Private methods called by GNSSocket and for tests.
|
||||
* Should not be used by the Nearby Socket client.
|
||||
*/
|
||||
@interface GNSSocket ()
|
||||
|
||||
@property(nonatomic, readwrite) UInt16 packetSize;
|
||||
@property(nonatomic, readonly) UInt8 receivePacketCounter;
|
||||
@property(nonatomic, readonly) UInt8 sendPacketCounter;
|
||||
@property(nonatomic, readonly) id<GNSSocketOwner> owner;
|
||||
@property(nonatomic, readonly) dispatch_queue_t queue;
|
||||
|
||||
- (instancetype)initWithOwner:(id<GNSSocketOwner>)owner
|
||||
centralPeer:(CBCentral *)centralPeer
|
||||
queue:(dispatch_queue_t)queue;
|
||||
- (instancetype)initWithOwner:(id<GNSSocketOwner>)owner
|
||||
peripheralPeer:(CBPeripheral *)peripheralPeer
|
||||
queue:(dispatch_queue_t)queue;
|
||||
|
||||
/**
|
||||
* Increments |receivePacketCounter|.
|
||||
*/
|
||||
- (void)incrementReceivePacketCounter;
|
||||
|
||||
/**
|
||||
* Increments |sendPacketCounter|.
|
||||
*/
|
||||
- (void)incrementSendPacketCounter;
|
||||
|
||||
/**
|
||||
* Called by the socket owner once the connection response packet has been sent or received.
|
||||
* The socket is ready to be used when this method is called.
|
||||
*/
|
||||
- (void)didConnect;
|
||||
|
||||
/**
|
||||
* Called by the socket owner when the socket is disconnected and should not be used anymore.
|
||||
* The socket delegate is called to be notified with:
|
||||
* -[id<GNSSocketDelegate> socket:didDisconnectWithError:]
|
||||
*
|
||||
* @param error Error that caused the socket to disconnect. Nil if the socket was disconnected
|
||||
* properly.
|
||||
*/
|
||||
- (void)didDisconnectWithError:(NSError *)error;
|
||||
|
||||
/**
|
||||
* Called by the socket owner when a data packet is received.
|
||||
*
|
||||
* @param dataPacket The data packet received.
|
||||
*/
|
||||
- (void)didReceiveIncomingWeaveDataPacket:(GNSWeaveDataPacket *)dataPacket;
|
||||
|
||||
/**
|
||||
* Returns YES if an incoming message is pending and more chunks are waited.
|
||||
*
|
||||
* @return BOOL
|
||||
*/
|
||||
- (BOOL)waitingForIncomingData;
|
||||
|
||||
/**
|
||||
* Returns the peer casted as CBPeripheral. Asserts if the socket was created with another type.
|
||||
*
|
||||
* @return Peer.
|
||||
*/
|
||||
- (CBPeripheral *)peerAsPeripheral;
|
||||
|
||||
/**
|
||||
* Returns the peer casted as CBCentral. Asserts if the socket was created with another type.
|
||||
*
|
||||
* @return Peer.
|
||||
*/
|
||||
- (CBCentral *)peerAsCentral;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,155 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSUtils.h"
|
||||
|
||||
@class CBCentral;
|
||||
@class GNSPeripheralServiceManager;
|
||||
@class GNSSocket;
|
||||
|
||||
// |progress| contains the percentage of the message already sent.
|
||||
typedef void (^GNSProgressHandler)(float progress);
|
||||
|
||||
/**
|
||||
* Protocol for socket delegates. The socket delegate should be set when the socket is accepted
|
||||
* (when used with GNSPeripheralServiceManager) or when the socket is created (when used with
|
||||
* GNSCentralPeer).
|
||||
*/
|
||||
@protocol GNSSocketDelegate<NSObject>
|
||||
|
||||
/**
|
||||
* Called when the socket is ready to send or receive data.
|
||||
*
|
||||
* @param socket Socket
|
||||
*/
|
||||
- (void)socketDidConnect:(GNSSocket *)socket;
|
||||
|
||||
/**
|
||||
* Called when the socket has been disconnected by the central (because a disconnect signal was
|
||||
* received or -[GNSSocket disconnect] has been called).
|
||||
*
|
||||
* @param socket Socket
|
||||
* @param error Error that caused the socket to disconnect. Nil if the socket was disconnected
|
||||
* properly (by calling -[GNSSocket disconnect] on this side or the other side of the socket).
|
||||
*/
|
||||
- (void)socket:(GNSSocket *)socket didDisconnectWithError:(NSError *)error;
|
||||
|
||||
/**
|
||||
* Called when a new message has been received.
|
||||
*
|
||||
* @param socket Socket
|
||||
* @param data Message received
|
||||
*/
|
||||
- (void)socket:(GNSSocket *)socket didReceiveData:(NSData *)data;
|
||||
|
||||
@end
|
||||
|
||||
/**
|
||||
* This class is in charge of receiving and sending data between one central and one peripheral. It
|
||||
* is created and owned by GNSPeripheralServiceManager or GNSCentralPeer.
|
||||
*
|
||||
*
|
||||
* * To create a socket, see the GNSPeripheralManager or GNSCentralPeer documentation.
|
||||
* When the socket is created, set the socket delegate. As soon as it is ready to use, the delegate
|
||||
* is called:
|
||||
* - (void)socketDidConnect:(GNSSocket *)socket {
|
||||
* // the socket is ready to be used. To send or receive data.
|
||||
* }
|
||||
*
|
||||
* * To receive data:
|
||||
* - (void)socket:(GNSSocket *)socket didReceiveData:(NSData *)data {
|
||||
* NSLog(@"Data received from central %@", data);
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
* * To send data:
|
||||
* GNSErrorHandler completionHandler = ^(NSError *error) {
|
||||
* if (error) {
|
||||
* NSLog(@"Failed to send data")
|
||||
* } else {
|
||||
* NSLog(@"data has been sent");
|
||||
* }
|
||||
* }
|
||||
* [socket sendData:dataToSend
|
||||
* completion:completionHandler];
|
||||
*
|
||||
* * To disconnect:
|
||||
* [mySocket disconnect];
|
||||
*
|
||||
* * Once the socket is disconnected (by -[GNSSocket disconnect] or by the peer):
|
||||
* - (void)socket:(GNSSocket *)socket didDisconnectWithError:(NSError *)error {
|
||||
* NSLog(@"Socket disconnected, by peer");
|
||||
* ...
|
||||
* }
|
||||
*/
|
||||
@interface GNSSocket : NSObject
|
||||
|
||||
/**
|
||||
* The socket delegate.
|
||||
*/
|
||||
@property(nonatomic, weak) id<GNSSocketDelegate> delegate;
|
||||
|
||||
/**
|
||||
* YES if the socket is connected. The physical BLE connection may still exist when this is NO.
|
||||
*/
|
||||
@property(nonatomic, readonly, getter=isConnected) BOOL connected;
|
||||
|
||||
/**
|
||||
* The Core Bluetooth peer identifier.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSUUID *peerIdentifier;
|
||||
|
||||
/**
|
||||
* The Core Bluetooth service identifier.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSUUID *serviceIdentifier;
|
||||
|
||||
/**
|
||||
* The socket identifier.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSUUID *socketIdentifier;
|
||||
|
||||
/**
|
||||
* A peer socket is always created by the library.
|
||||
*/
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
/**
|
||||
* Returns YES if there is already a send operation in progress. If this method returns NO,
|
||||
* then a call to -[GNSSocket sendData:completion:] won't result in a |GNSErrorOperationInProgress|
|
||||
* error.
|
||||
*/
|
||||
- (BOOL)isSendOperationInProgress;
|
||||
|
||||
/**
|
||||
* Sends data to the central. The completion has to be called before calling this method a second
|
||||
* time.
|
||||
*
|
||||
* @param data Data to send
|
||||
* @param progressHandler Called repeatedly for progress feedback while the data is being sent.
|
||||
* @param completion Callback called when the data has been fully sent,
|
||||
* or it has failed to be sent (socket not connected or disconnected).
|
||||
*/
|
||||
- (void)sendData:(NSData *)data
|
||||
progressHandler:(GNSProgressHandler)progressHandler
|
||||
completion:(GNSErrorHandler)completion;
|
||||
|
||||
/**
|
||||
* Sends the disconnect signal and then disconnects this connection. The socket cannot be
|
||||
* disconnected if the socket is processing data to send. The current send data operation has
|
||||
* to be cancelled first.
|
||||
*/
|
||||
- (void)disconnect;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,238 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSSocket+Private.h"
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSWeavePacket.h"
|
||||
#import "GoogleToolboxForMac/GTMLogger.h"
|
||||
|
||||
typedef void (^GNSIncomingChunkReceivedBlock)(NSData *incomingData);
|
||||
|
||||
@interface GNSSocket () {
|
||||
NSMutableData *_incomingBuffer;
|
||||
id _peer;
|
||||
}
|
||||
|
||||
// Handler to generate a chunk for sending data. When the bluetooth stack is ready to send
|
||||
// more data the handler is called recursively to send the next chunk.
|
||||
@property(nonatomic, copy) GNSSendChunkBlock sendChunkCallback;
|
||||
|
||||
// Handler to receive chunk from the central.
|
||||
@property(nonatomic, copy) GNSIncomingChunkReceivedBlock incomingChunkReceivedCallback;
|
||||
|
||||
@property(nonatomic, readwrite, assign, getter=isConnected) BOOL connected;
|
||||
|
||||
@property(nonatomic, readwrite) NSUUID *socketIdentifier;
|
||||
|
||||
- (instancetype)initWithOwner:(id<GNSSocketOwner>)owner
|
||||
peer:(id)peer
|
||||
queue:(dispatch_queue_t)queue NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
@end
|
||||
|
||||
@implementation GNSSocket
|
||||
|
||||
- (void)dealloc {
|
||||
[_owner socketWillBeDeallocated:self];
|
||||
}
|
||||
|
||||
- (BOOL)isSendOperationInProgress {
|
||||
return self.sendChunkCallback != nil;
|
||||
}
|
||||
|
||||
- (void)sendData:(NSData *)data
|
||||
progressHandler:(GNSProgressHandler)progressHandler
|
||||
completion:(GNSErrorHandler)completion {
|
||||
void (^callCompletion)(NSError *) = ^(NSError *error) {
|
||||
if (completion) dispatch_async(_queue, ^{ completion(error); });
|
||||
};
|
||||
|
||||
if (self.sendChunkCallback) {
|
||||
GTMLoggerInfo(@"Send operation already in progress");
|
||||
callCompletion(GNSErrorWithCode(GNSErrorOperationInProgress));
|
||||
return;
|
||||
}
|
||||
data = [data copy];
|
||||
NSUInteger totalDataSize = data.length;
|
||||
GTMLoggerInfo(@"Sending data with size %lu", (unsigned long)totalDataSize);
|
||||
|
||||
// Capture self for the duration of the send operation, to ensure it is completely sent.
|
||||
// If the connection is lost, the block will be deleted and the retain cycle broken.
|
||||
__typeof__(self) selfRef = self; // this avoids the retain cycle compiler warning
|
||||
self.sendChunkCallback = ^(NSUInteger offset) {
|
||||
__typeof__(self) self = selfRef;
|
||||
if (!self.isConnected) {
|
||||
self.sendChunkCallback = nil;
|
||||
callCompletion(GNSErrorWithCode(GNSErrorNoConnection));
|
||||
return;
|
||||
}
|
||||
if (progressHandler) {
|
||||
float progressPercentage = 1.0;
|
||||
if (totalDataSize > 0) {
|
||||
progressPercentage = (float)offset / totalDataSize;
|
||||
}
|
||||
progressHandler(progressPercentage);
|
||||
}
|
||||
NSUInteger newOffset = offset;
|
||||
GNSWeaveDataPacket *dataPacket =
|
||||
[GNSWeaveDataPacket dataPacketWithPacketCounter:self.sendPacketCounter
|
||||
packetSize:self.packetSize
|
||||
data:data
|
||||
offset:&newOffset];
|
||||
[self incrementSendPacketCounter];
|
||||
|
||||
GTMLoggerInfo(@"Sending chunk with size %ld", (long)(newOffset - offset));
|
||||
[self.owner sendData:[dataPacket serialize] socket:self completion:^(NSError *_Nullable error) {
|
||||
if (error) {
|
||||
GTMLoggerInfo(@"Error sending chunk");
|
||||
self.sendChunkCallback = nil;
|
||||
callCompletion(error);
|
||||
} else {
|
||||
if (newOffset < totalDataSize) {
|
||||
// Dispatch async to avoid stack overflow on large payloads.
|
||||
dispatch_async(self.queue, ^{ self.sendChunkCallback(newOffset); });
|
||||
} else {
|
||||
GTMLoggerInfo(@"Finished sending payload");
|
||||
self.sendChunkCallback = nil;
|
||||
callCompletion(nil);
|
||||
}
|
||||
}
|
||||
}];
|
||||
};
|
||||
self.sendChunkCallback(0);
|
||||
}
|
||||
|
||||
- (void)disconnect {
|
||||
if (!_connected) {
|
||||
GTMLoggerInfo(@"Socket already disconnected, socket: %@, delegate %@, owner %@", self,
|
||||
_delegate, _owner);
|
||||
return;
|
||||
}
|
||||
GTMLoggerInfo(@"Disconnect");
|
||||
[_owner disconnectSocket:self];
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
return [NSString stringWithFormat:@"<%@: %p, %@, central: %@>", [self class], self,
|
||||
_connected ? @"connected" : @"not connected",
|
||||
self.peerIdentifier.UUIDString];
|
||||
}
|
||||
|
||||
- (NSUUID *)peerIdentifier {
|
||||
return (NSUUID *)[_peer identifier];
|
||||
}
|
||||
|
||||
- (NSUUID *)serviceIdentifier {
|
||||
return [_owner socketServiceIdentifier:self];
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (instancetype)init {
|
||||
[self doesNotRecognizeSelector:_cmd];
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (instancetype)initWithOwner:(id<GNSSocketOwner>)owner
|
||||
centralPeer:(CBCentral *)centralPeer
|
||||
queue:(dispatch_queue_t)queue {
|
||||
return [self initWithOwner:owner peer:centralPeer queue:queue];
|
||||
}
|
||||
|
||||
- (instancetype)initWithOwner:(id<GNSSocketOwner>)owner
|
||||
peripheralPeer:(CBPeripheral *)peripheralPeer
|
||||
queue:(dispatch_queue_t)queue {
|
||||
return [self initWithOwner:owner peer:peripheralPeer queue:queue];
|
||||
}
|
||||
|
||||
- (instancetype)initWithOwner:(id<GNSSocketOwner>)owner
|
||||
peer:(id)peer
|
||||
queue:(dispatch_queue_t)queue {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
NSAssert(owner, @"Socket should have an owner.");
|
||||
NSAssert(peer, @"Socket should have a peer.");
|
||||
_owner = owner;
|
||||
_peer = peer;
|
||||
_queue = queue;
|
||||
_socketIdentifier = [NSUUID UUID];
|
||||
_packetSize = kGNSMinSupportedPacketSize;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)incrementReceivePacketCounter {
|
||||
_receivePacketCounter = (_receivePacketCounter + 1) % kGNSMaxPacketCounterValue;
|
||||
GTMLoggerDebug(@"New receive packet counter %d", _receivePacketCounter);
|
||||
}
|
||||
|
||||
- (void)incrementSendPacketCounter {
|
||||
_sendPacketCounter = (_sendPacketCounter + 1) % kGNSMaxPacketCounterValue;
|
||||
GTMLoggerDebug(@"New send packet counter %d", _sendPacketCounter);
|
||||
}
|
||||
|
||||
- (void)didConnect {
|
||||
_connected = YES;
|
||||
[_delegate socketDidConnect:self];
|
||||
}
|
||||
|
||||
- (void)didDisconnectWithError:(NSError *)error {
|
||||
_connected = NO;
|
||||
_incomingChunkReceivedCallback = nil;
|
||||
_incomingBuffer = nil;
|
||||
[_delegate socket:self didDisconnectWithError:error];
|
||||
}
|
||||
|
||||
- (void)didReceiveIncomingWeaveDataPacket:(GNSWeaveDataPacket *)dataPacket {
|
||||
if (!_connected) {
|
||||
GTMLoggerError(@"Cannot receive incoming data packet while not being connected");
|
||||
return;
|
||||
}
|
||||
if (dataPacket.isFirstPacket) {
|
||||
NSAssert(!_incomingBuffer, @"There should not be a receive operation in progress.");
|
||||
_incomingBuffer = [NSMutableData data];
|
||||
}
|
||||
GTMLoggerInfo(@"Received chunk with size %lu", (unsigned long)dataPacket.data.length);
|
||||
[_incomingBuffer appendData:dataPacket.data];
|
||||
if (dataPacket.isLastPacket) {
|
||||
NSData *incomingData = _incomingBuffer;
|
||||
_incomingBuffer = nil;
|
||||
GTMLoggerInfo(@"Finished receiving payload with size %lu", (unsigned long)incomingData.length);
|
||||
[self.delegate socket:self didReceiveData:incomingData];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)waitingForIncomingData {
|
||||
return _incomingBuffer != nil;
|
||||
}
|
||||
|
||||
- (CBPeripheral *)peerAsPeripheral {
|
||||
NSAssert([_peer isKindOfClass:[CBPeripheral class]], @"Wrong peer type %@", _peer);
|
||||
if ([_peer isKindOfClass:[CBPeripheral class]]) {
|
||||
return _peer;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (CBCentral *)peerAsCentral {
|
||||
NSAssert([_peer isKindOfClass:[CBCentral class]], @"Wrong peer type %@", _peer);
|
||||
if ([_peer isKindOfClass:[CBCentral class]]) {
|
||||
return _peer;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSUtils.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
extern NSString *const kGNSWeaveToPeripheralCharUUIDString;
|
||||
extern NSString *const kGNSWeaveFromPeripheralCharUUIDString;
|
||||
|
||||
extern NSString *const kGNSPairingCharUUIDString;
|
||||
|
||||
@protocol GNSPeer;
|
||||
|
||||
typedef void (^GNSBoolHandler)(BOOL flag);
|
||||
typedef void (^GNSSendChunkBlock)(NSUInteger offset);
|
||||
|
||||
/**
|
||||
* Returns NSError with the description.
|
||||
*
|
||||
* @param errorCode Error code from GNSError.
|
||||
*
|
||||
* @return NSError.
|
||||
*/
|
||||
NSError *GNSErrorWithCode(GNSError errorCode);
|
||||
|
||||
/**
|
||||
* Returns a human readable name based on a characteristic UUID.
|
||||
*
|
||||
* @param uuid Characteristic UUID string
|
||||
*
|
||||
* @return Name of the characteristic.
|
||||
*/
|
||||
NSString *GNSCharacteristicName(NSString *uuid);
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import <CoreBluetooth/CoreBluetooth.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
extern NSString *const kGNSSocketsErrorDomain;
|
||||
|
||||
typedef NS_ENUM(NSInteger, GNSError) {
|
||||
GNSErrorNoError,
|
||||
GNSErrorNoConnection,
|
||||
GNSErrorLostConnection,
|
||||
GNSErrorOperationInProgress,
|
||||
GNSErrorMissingService,
|
||||
GNSErrorMissingCharacteristics,
|
||||
GNSErrorPeripheralDidRefuseConnection,
|
||||
GNSErrorCancelPendingSocketRequested,
|
||||
GNSErrorNewInviteToConnectReceived,
|
||||
GNSErrorWeaveErrorPacketReceived,
|
||||
GNSErrorUnsupportedWeaveProtocolVersion,
|
||||
GNSErrorUnexpectedWeaveControlPacket,
|
||||
GNSErrorParsingWeavePacket,
|
||||
GNSErrorWrongWeavePacketCounter,
|
||||
GNSErrorWeaveDataTransferInProgress,
|
||||
GNSErrorParsingWeavePacketTooSmall,
|
||||
GNSErrorParsingWeavePacketTooLarge,
|
||||
GNSErrorConnectionTimedOut,
|
||||
};
|
||||
|
||||
// This handler is called to receive the CBPeripheralManager state updates.
|
||||
typedef void (^GNSPeripheralManagerStateHandler)(CBManagerState peripheralManagerState);
|
||||
|
||||
typedef void (^GNSErrorHandler)(NSError *_Nullable error);
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSUtils+Private.h"
|
||||
|
||||
#import <CommonCrypto/CommonDigest.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
NSString *const kGNSSocketsErrorDomain = @"com.google.nearby.sockets";
|
||||
|
||||
NSString *const kGNSWeaveToPeripheralCharUUIDString = @"00000100-0004-1000-8000-001A11000101";
|
||||
NSString *const kGNSWeaveFromPeripheralCharUUIDString = @"00000100-0004-1000-8000-001A11000102";
|
||||
|
||||
NSString *const kGNSPairingCharUUIDString = @"17836FBD-8C6A-4B81-83CE-8560629E834B";
|
||||
|
||||
NSError *GNSErrorWithCode(GNSError errorCode) {
|
||||
NSString *description = nil;
|
||||
switch (errorCode) {
|
||||
case GNSErrorNoError:
|
||||
NSCAssert(NO, @"Should not create an error with GNSErrorNoError");
|
||||
break;
|
||||
case GNSErrorNoConnection:
|
||||
description = @"No connection.";
|
||||
break;
|
||||
case GNSErrorLostConnection:
|
||||
description = @"Connection lost.";
|
||||
break;
|
||||
case GNSErrorOperationInProgress:
|
||||
description = @"Operation in progress.";
|
||||
break;
|
||||
case GNSErrorMissingService:
|
||||
description = @"Missing service.";
|
||||
break;
|
||||
case GNSErrorMissingCharacteristics:
|
||||
description = @"Missing characteristic.";
|
||||
break;
|
||||
case GNSErrorPeripheralDidRefuseConnection:
|
||||
description = @"Peripheral refused connection.";
|
||||
break;
|
||||
case GNSErrorCancelPendingSocketRequested:
|
||||
description = @"Pending socket request canceled.";
|
||||
break;
|
||||
case GNSErrorNewInviteToConnectReceived:
|
||||
description = @"Second invitation to connect received";
|
||||
break;
|
||||
case GNSErrorWeaveErrorPacketReceived:
|
||||
description = @"Weave error packet received.";
|
||||
break;
|
||||
case GNSErrorUnsupportedWeaveProtocolVersion:
|
||||
description = @"Unsupported weave protocol version.";
|
||||
break;
|
||||
case GNSErrorUnexpectedWeaveControlPacket:
|
||||
description = @"Unexpected weave control packet.";
|
||||
break;
|
||||
case GNSErrorParsingWeavePacket:
|
||||
description = @"Parsing weave packet error.";
|
||||
break;
|
||||
case GNSErrorWrongWeavePacketCounter:
|
||||
description = @"Wrong weave packet counter.";
|
||||
break;
|
||||
case GNSErrorWeaveDataTransferInProgress:
|
||||
description = @"Weave data transfer in progress.";
|
||||
break;
|
||||
case GNSErrorParsingWeavePacketTooSmall:
|
||||
description = @"Weave packet too small.";
|
||||
break;
|
||||
case GNSErrorParsingWeavePacketTooLarge:
|
||||
description = @"Weave packet too large.";
|
||||
break;
|
||||
case GNSErrorConnectionTimedOut:
|
||||
description = @"Connection timed out.";
|
||||
break;
|
||||
}
|
||||
NSCAssert(description, @"Unknown NearbySocket error code %ld", (long)errorCode);
|
||||
NSDictionary<NSErrorUserInfoKey, id> *userInfo = @{NSLocalizedDescriptionKey : description};
|
||||
return [NSError errorWithDomain:kGNSSocketsErrorDomain code:errorCode userInfo:userInfo];
|
||||
}
|
||||
|
||||
NSString *GNSCharacteristicName(NSString *uuid) {
|
||||
if ([uuid isEqual:kGNSWeaveToPeripheralCharUUIDString]) {
|
||||
return @"ToPeripheralChar";
|
||||
} else if ([uuid isEqual:kGNSWeaveFromPeripheralCharUUIDString]) {
|
||||
return @"FromPeripheralChar";
|
||||
} else if ([uuid isEqual:kGNSPairingCharUUIDString]) {
|
||||
return @"PairingChar";
|
||||
}
|
||||
return @"UnknownChar";
|
||||
}
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class GNSWeaveConnectionRequestPacket;
|
||||
@class GNSWeaveConnectionConfirmPacket;
|
||||
@class GNSWeaveErrorPacket;
|
||||
@class GNSWeaveDataPacket;
|
||||
|
||||
extern const UInt8 kGNSMaxPacketCounterValue;
|
||||
extern const UInt16 kGNSMinSupportedPacketSize;
|
||||
extern const NSUInteger kGNSMaxCentralHandshakeDataSize;
|
||||
|
||||
typedef NS_ENUM(UInt8, GNSWeaveControlCommand) {
|
||||
GNSWeaveControlCommandConnectionRequest = 0,
|
||||
GNSWeaveControlCommandConnectionConfirm = 1,
|
||||
GNSWeaveControlCommandError = 2,
|
||||
};
|
||||
|
||||
/**
|
||||
* This protocol should be implemented by classes handling Weave BLE packets. The classes should
|
||||
* implement only the methods corresponding to the packets it should handle. It should be used with
|
||||
* +[GNSWeavePacket parsePacket:] and -[GNSWeavePacket visitWithHandler:context:] to parse
|
||||
* serialized Weave packets.
|
||||
*
|
||||
* For example, the |Socket| class below handles a serialized weave connection request packet:
|
||||
*
|
||||
* @interface Socket : NSObject<GNSWeavePacketHandler>
|
||||
* @end
|
||||
*
|
||||
* @implementation Socket
|
||||
*
|
||||
* - (void)didReceivedData:(NSData *)data {
|
||||
* GNSWeavePacket *packet = [GNSWeavePacket parseData:data error:nil];
|
||||
* if ([packet visitWithHandler:self context:nil]) {
|
||||
* NSLog(@"This class can handle this packet.");
|
||||
* } else {
|
||||
* NSLog(@"Unexpected packet received.");
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* - (void)handleConnectionRequestPacket:(GNSWeaveConnectionRequestPacket *)packet
|
||||
* context:(nullable id)context {
|
||||
* NSLog(@"Connection request packet received.");
|
||||
* ...
|
||||
* }
|
||||
* @end
|
||||
*
|
||||
**/
|
||||
@protocol GNSWeavePacketHandler<NSObject>
|
||||
@optional
|
||||
|
||||
- (void)handleConnectionRequestPacket:(GNSWeaveConnectionRequestPacket *)packet
|
||||
context:(nullable id)context;
|
||||
- (void)handleConnectionConfirmPacket:(GNSWeaveConnectionConfirmPacket *)packet
|
||||
context:(nullable id)context;
|
||||
- (void)handleErrorPacket:(GNSWeaveErrorPacket *)packet context:(nullable id)context;
|
||||
- (void)handleDataPacket:(GNSWeaveDataPacket *)packet context:(nullable id)context;
|
||||
|
||||
@end
|
||||
|
||||
/**
|
||||
* The Weave BLE protocol (go/weave-ble-gatt-transport) has two types of packets: control and data.
|
||||
*
|
||||
* There are 3 types of control packets:
|
||||
* - connection request (GNSWeaveConnectionRequestPacket);
|
||||
* - connection confirm (GNSWeaveConnectionConfirmPacket);
|
||||
* - error (GNSWeaveErrorPacket).
|
||||
*
|
||||
* The first two messages are used to establish the Weave BLE logical connection: the central
|
||||
* (client) sends a connection request packet and the peripheral replies with a connection confirm
|
||||
* request. This first two messages are used to negociate to connection paramenter: protocol version
|
||||
* and packet size.
|
||||
*
|
||||
* After the logical connection is established the peers can exchange arbitrarily large messages
|
||||
* that split are into data packets (GNSWeaveDataPacket).
|
||||
*
|
||||
* All packets have a 3-bit packet counter. This is used to detect packet drops, re-ordering or
|
||||
* duplication. There is no recovery strategy, if a peer detects any error it sends an error packet
|
||||
* to the other peer and closes the connection.
|
||||
**/
|
||||
@interface GNSWeavePacket : NSObject
|
||||
@property(nonatomic, readonly) UInt8 packetCounter;
|
||||
|
||||
/**
|
||||
* Parses |data| and, if possible, extracts and return the corresponding Weave packet. It returns
|
||||
* nil if there was an error parsing the packet. See GNSWeavePacketHandler.
|
||||
*
|
||||
* @param data The binary data containing.
|
||||
* @param outError The error causing the parsing to fail.
|
||||
*
|
||||
* @return The corresponding Weave packet or an nil if there was an error.
|
||||
**/
|
||||
+ (nullable GNSWeavePacket *)parseData:(NSData *)data
|
||||
error:(out __autoreleasing NSError **)outError;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
/**
|
||||
* Calls the |handler| method corresponding to the type of the current packet. See
|
||||
* GNSWeavePacketHandler.
|
||||
*
|
||||
* @param handler The packet handler.
|
||||
* @param context The context passed to the handler.
|
||||
*
|
||||
* @return YES if |handler| can handle the current message.
|
||||
*/
|
||||
- (BOOL)visitWithHandler:(id<GNSWeavePacketHandler>)handler context:(nullable id)context;
|
||||
|
||||
/**
|
||||
* Serialize the packet.
|
||||
*/
|
||||
- (NSData *)serialize;
|
||||
|
||||
@end
|
||||
|
||||
@interface GNSWeaveConnectionRequestPacket : GNSWeavePacket
|
||||
|
||||
@property(nonatomic, readonly) UInt16 minVersion;
|
||||
@property(nonatomic, readonly) UInt16 maxVersion;
|
||||
@property(nonatomic, readonly) UInt16 maxPacketSize;
|
||||
@property(nonatomic, readonly) NSData *data;
|
||||
|
||||
/**
|
||||
* Creates an instance of the GNSWeaveConnectionRequestPacket. Note: there is no packet counter
|
||||
* parameter as this is necessarily the first packet sent by this peer (central/client).
|
||||
*
|
||||
* @param minVersion The minimum Weave BLE protocol version supported by this peer (central/client).
|
||||
* @param maxVersion The maximum Weave BLE protocol version supported by this peer.
|
||||
* @param maxPacketSize The maximum packet size (in bytes) supported by this peer. According to the
|
||||
* BLE specs this should be at least 20 and at most 509 bytes.
|
||||
* @param data The optional data send with the connection request packet. This should not exceed
|
||||
* 13 bytes
|
||||
*
|
||||
* @return GNSWeaveConnectionRequestPacket instance.
|
||||
**/
|
||||
- (nullable instancetype)initWithMinVersion:(UInt16)minVersion
|
||||
maxVersion:(UInt16)maxVersion
|
||||
maxPacketSize:(UInt16)maxPacketSize
|
||||
data:(nullable NSData *)data NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
- (instancetype)initWithPacketCounter:(UInt8)packetCounter NS_UNAVAILABLE;
|
||||
|
||||
@end
|
||||
|
||||
@interface GNSWeaveConnectionConfirmPacket : GNSWeavePacket
|
||||
|
||||
@property(nonatomic, readonly) UInt16 version;
|
||||
@property(nonatomic, readonly) UInt16 packetSize;
|
||||
@property(nonatomic, readonly) NSData *data;
|
||||
|
||||
/**
|
||||
* Creates an instance of the GNSWeaveConnectionConfirmPacket. Note: there is no packet counter
|
||||
* parameter as this is necessarily the first packet sent by this peer (peripheral/server).
|
||||
*
|
||||
* @param version The chosen Weave BLE protocol version for the current connection.
|
||||
* @param packetSize The chosen packet size for the current connection. According to the BLE specs
|
||||
* this should be at least 20 and at most 509 bytes.
|
||||
* @param data The optional data send with the connection confirm packet. This should not exceed 15
|
||||
* bytes.
|
||||
*
|
||||
* @return GNSWeaveConnectionConfirmPacket instance.
|
||||
**/
|
||||
- (nullable instancetype)initWithVersion:(UInt16)version
|
||||
packetSize:(UInt16)packetSize
|
||||
data:(nullable NSData *)data NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
- (instancetype)initWithPacketCounter:(UInt8)packetCounter NS_UNAVAILABLE;
|
||||
|
||||
@end
|
||||
|
||||
@interface GNSWeaveErrorPacket : GNSWeavePacket
|
||||
|
||||
/**
|
||||
* Creates an instance of the GNSWeaveErrorPacket.
|
||||
*
|
||||
* @param packetCounter The current 3-bit packet counter (i.e. should be stricly smaller than 8).
|
||||
*
|
||||
* @return GNSWeaveErrorPacket instance.
|
||||
**/
|
||||
- (nullable instancetype)initWithPacketCounter:(UInt8)packetCounter NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
@end
|
||||
|
||||
@interface GNSWeaveDataPacket : GNSWeavePacket
|
||||
@property(nonatomic, readonly, getter=isFirstPacket) BOOL firstPacket;
|
||||
@property(nonatomic, readonly, getter=isLastPacket) BOOL lastPacket;
|
||||
@property(nonatomic, readonly) NSData *data;
|
||||
|
||||
/**
|
||||
* Creates an instance of the GNSWeaveDataPacket for |data| starting at |outOffset| containing at
|
||||
* most |packetSize|-1 bytes, and updates |outOffset| for the next packet. This should be used
|
||||
* iteratively to split a message in GNSWeaveDataPacket's to be send to other peer.
|
||||
*
|
||||
* @param packetCounter The current 3-bit packet counter (i.e. should be stricly smaller than 8).
|
||||
* @param packetSize The maximum size of a data packet (including the 1-byte header).
|
||||
* @param data The data to send to the other peer.
|
||||
* @param inOutOffset The offset for |data|. It's updated with the new offset value for the next
|
||||
* data packet. It's equal to |data.length| if this is the last packet.
|
||||
*
|
||||
* @return GNSWeaveDataPacket instance.
|
||||
**/
|
||||
+ (nullable GNSWeaveDataPacket *)dataPacketWithPacketCounter:(UInt8)packetCounter
|
||||
packetSize:(UInt16)packetSize
|
||||
data:(NSData *)data
|
||||
offset:(NSUInteger *)inOutOffset;
|
||||
|
||||
/**
|
||||
* Creates an instance of the GNSWeaveDataPacket. Avoid using this initializer directly, prefer
|
||||
* using -[GNSWeaveDataPacket dataPacketWithPacketCounter:packetSize:data:offset:].
|
||||
*
|
||||
* @param packetCounter The current 3-bit packet counter (i.e. should be stricly smaller than 8).
|
||||
* @param isFirstPacket YES if this is the first packet of a message.
|
||||
* @param isLastPacket YES if this is the last packet of a message.
|
||||
* @param data The actual data being send.
|
||||
*
|
||||
* @return GNSWeaveDataPacket instance.
|
||||
**/
|
||||
- (nullable instancetype)initWithPacketCounter:(UInt8)packetCounter
|
||||
firstPacket:(BOOL)isFirstPacket
|
||||
lastPacket:(BOOL)isLastPacket
|
||||
data:(NSData *)data NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
- (instancetype)initWithPacketCounter:(UInt8)packetCounter NS_UNAVAILABLE;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
+416
@@ -0,0 +1,416 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSWeavePacket.h"
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/Sockets/Source/Shared/GNSUtils+Private.h"
|
||||
|
||||
// Constants defined by the Weave BLE protocol.
|
||||
const UInt8 kGNSMaxPacketCounterValue = 8;
|
||||
const UInt16 kGNSMinSupportedPacketSize = 20;
|
||||
const NSUInteger kGNSMaxCentralHandshakeDataSize = 13;
|
||||
static const NSUInteger kHeaderSize = 1;
|
||||
static const NSUInteger kMaxControlPacketSize = 20;
|
||||
static const NSUInteger kMinConnectionRequestPayloadSize = 6;
|
||||
static const NSUInteger kMinConnectionConfirmPayloadSize = 4;
|
||||
|
||||
// Header offsets.
|
||||
static const UInt8 kPacketTypeOffset = 7;
|
||||
static const UInt8 kPacketCounterOffset = 4;
|
||||
static const UInt8 kFirstPacketFlagOffset = 3;
|
||||
static const UInt8 kLastPacketFlagOffset = 2;
|
||||
|
||||
// Bitmasks for the packet headers.
|
||||
static const UInt8 kPacketTypeBitmask = (1 << 7); // 10000000
|
||||
static const UInt8 kPacketCounterBitmask = (1 << 6) | (1 << 5) | (1 << 4); // 01110000
|
||||
static const UInt8 kControlCommandBitmask = (1 << 3) | (1 << 2) | (1 << 1) | (1 << 0); // 00001111
|
||||
static const UInt8 kFirstPacketFlagBitmask = (1 << 3); // 00001000
|
||||
static const UInt8 kLastPacketFlagBitmask = (1 << 2); // 00000100
|
||||
|
||||
static const UInt8 kControlPacketValue = 1;
|
||||
|
||||
/**
|
||||
* The Weave BLE protocol (go/weave-ble-gatt-transport) has two types of packets: control and data.
|
||||
* Both packets contain a 1-byte header followed by a variable size payload. The header
|
||||
* bit-structure is the following:
|
||||
*
|
||||
* 1) Control Packet: TCCCNNNN
|
||||
* T: packet type, 1 for control packets
|
||||
* CCC: packet counter
|
||||
* NNNN: control command number (GNSWeaveControlCommand)
|
||||
*
|
||||
* 2) Data Packet: TCCCFL00
|
||||
* T: packet type, 0 for data packets
|
||||
* CCC: packet counter
|
||||
* F: bit indicating the first packet of a message
|
||||
* L: bit indication the last packet of a message
|
||||
*
|
||||
* Note: A single packet message will have both F and L set.
|
||||
*
|
||||
* The helpers below are used to manipulate the headers.
|
||||
**/
|
||||
static UInt8 ExtractPacketType(UInt8 header) {
|
||||
return (header & kPacketTypeBitmask) >> kPacketTypeOffset;
|
||||
}
|
||||
|
||||
static UInt8 ExtractPacketCounter(UInt8 header) {
|
||||
return (header & kPacketCounterBitmask) >> kPacketCounterOffset;
|
||||
}
|
||||
|
||||
static UInt8 ExtractControlCommand(UInt8 header) { return (header & kControlCommandBitmask); }
|
||||
|
||||
static UInt8 ExtractFirstPacketFlag(UInt8 header) {
|
||||
return (header & kFirstPacketFlagBitmask) >> kFirstPacketFlagOffset;
|
||||
}
|
||||
|
||||
static UInt8 ExtractLastPacketFlag(UInt8 header) {
|
||||
return (header & kLastPacketFlagBitmask) >> kLastPacketFlagOffset;
|
||||
}
|
||||
|
||||
// The Weave protocol uses big-endian format (network byte order) for multi-byte types.
|
||||
static UInt16 ExtractUInt16(const void *bytes, size_t offset) {
|
||||
return CFSwapInt16BigToHost(*(UInt16 *)(bytes + offset));
|
||||
}
|
||||
|
||||
static NSData *ExtractPayloadData(NSData *payload, size_t offset) {
|
||||
if (offset >= payload.length) {
|
||||
return nil;
|
||||
}
|
||||
return [payload subdataWithRange:NSMakeRange(offset, payload.length - offset)];
|
||||
}
|
||||
|
||||
static UInt8 WeaveControlPacketHeader(UInt8 packetCounter, GNSWeaveControlCommand command) {
|
||||
return (1 << kPacketTypeOffset) | (packetCounter << kPacketCounterOffset) | command;
|
||||
}
|
||||
|
||||
static UInt8 WeaveDataPacketHeader(UInt8 packetCounter, BOOL firstPacketFlag, BOOL lastPacketFlag) {
|
||||
return (packetCounter << kPacketCounterOffset) | (firstPacketFlag << kFirstPacketFlagOffset) |
|
||||
(lastPacketFlag << kLastPacketFlagOffset);
|
||||
}
|
||||
|
||||
@interface GNSWeavePacket ()
|
||||
|
||||
- (nullable instancetype)initWithPacketCounter:(UInt8)packetCounter NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
@end
|
||||
|
||||
@implementation GNSWeavePacket
|
||||
|
||||
+ (GNSWeavePacket *)parseData:(NSData *)data error:(out __autoreleasing NSError **)outError {
|
||||
if (data.length < kHeaderSize) {
|
||||
if (outError) {
|
||||
*outError = GNSErrorWithCode(GNSErrorParsingWeavePacketTooSmall);
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
// The header is the first byte of |data|.
|
||||
UInt8 header = *(UInt8 *)data.bytes;
|
||||
BOOL isControlPacket = ExtractPacketType(header) == kControlPacketValue;
|
||||
UInt8 packetCounter = ExtractPacketCounter(header);
|
||||
|
||||
GNSWeavePacket *parsedPacket = nil;
|
||||
if (isControlPacket) {
|
||||
if (data.length > kMaxControlPacketSize) {
|
||||
if (outError) {
|
||||
*outError = GNSErrorWithCode(GNSErrorParsingWeavePacketTooLarge);
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
GNSWeaveControlCommand controlCommand = ExtractControlCommand(header);
|
||||
switch (controlCommand) {
|
||||
case GNSWeaveControlCommandConnectionRequest: {
|
||||
NSData *payload = [data subdataWithRange:NSMakeRange(1, data.length - 1)];
|
||||
if (payload.length < kMinConnectionRequestPayloadSize) {
|
||||
if (outError) {
|
||||
*outError = GNSErrorWithCode(GNSErrorParsingWeavePacketTooSmall);
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
UInt16 minVersion = ExtractUInt16(payload.bytes, 0);
|
||||
UInt16 maxVersion = ExtractUInt16(payload.bytes, sizeof(minVersion));
|
||||
UInt16 maxPacketSize =
|
||||
ExtractUInt16(payload.bytes, sizeof(minVersion) + sizeof(maxVersion));
|
||||
|
||||
// Extracting the (optional) payload data.
|
||||
size_t payloadDataOffset = sizeof(maxVersion) + sizeof(minVersion) + sizeof(maxPacketSize);
|
||||
NSData *payloadData = ExtractPayloadData(payload, payloadDataOffset);
|
||||
parsedPacket = [[GNSWeaveConnectionRequestPacket alloc] initWithMinVersion:minVersion
|
||||
maxVersion:maxVersion
|
||||
maxPacketSize:maxPacketSize
|
||||
data:payloadData];
|
||||
break;
|
||||
}
|
||||
case GNSWeaveControlCommandConnectionConfirm: {
|
||||
NSData *payload = [data subdataWithRange:NSMakeRange(1, data.length - 1)];
|
||||
if (payload.length < kMinConnectionConfirmPayloadSize) {
|
||||
if (outError) {
|
||||
*outError = GNSErrorWithCode(GNSErrorParsingWeavePacketTooSmall);
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
UInt16 version = ExtractUInt16(payload.bytes, 0);
|
||||
UInt16 packetSize = ExtractUInt16(payload.bytes, sizeof(version));
|
||||
size_t payloadDataOffset = sizeof(version) + sizeof(packetSize);
|
||||
|
||||
// Extracting the (optional) payload data.
|
||||
NSData *payloadData = ExtractPayloadData(payload, payloadDataOffset);
|
||||
parsedPacket = [[GNSWeaveConnectionConfirmPacket alloc] initWithVersion:version
|
||||
packetSize:packetSize
|
||||
data:payloadData];
|
||||
break;
|
||||
}
|
||||
case GNSWeaveControlCommandError: {
|
||||
parsedPacket = [[GNSWeaveErrorPacket alloc] initWithPacketCounter:packetCounter];
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
parsedPacket = [[GNSWeaveDataPacket alloc]
|
||||
initWithPacketCounter:packetCounter
|
||||
firstPacket:ExtractFirstPacketFlag(header)
|
||||
lastPacket:ExtractLastPacketFlag(header)
|
||||
data:[data subdataWithRange:NSMakeRange(1, data.length - 1)]];
|
||||
}
|
||||
NSAssert(parsedPacket != nil, @"Parsed packet cannot be nil.");
|
||||
return parsedPacket;
|
||||
}
|
||||
|
||||
- (instancetype)initWithPacketCounter:(UInt8)packetCounter {
|
||||
NSAssert(packetCounter < kGNSMaxPacketCounterValue,
|
||||
@"The packetCounter should have at most 3 bits.");
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_packetCounter = packetCounter;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
[self doesNotRecognizeSelector:_cmd];
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (BOOL)visitWithHandler:(id<GNSWeavePacketHandler>)handler context:(id)context {
|
||||
[self doesNotRecognizeSelector:_cmd];
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (NSData *)serialize {
|
||||
[self doesNotRecognizeSelector:_cmd];
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
return [NSString
|
||||
stringWithFormat:@"<%@: %p, packet counter: %d>", [self class], self, _packetCounter];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation GNSWeaveConnectionRequestPacket
|
||||
|
||||
- (instancetype)initWithMinVersion:(UInt16)minVersion
|
||||
maxVersion:(UInt16)maxVersion
|
||||
maxPacketSize:(UInt16)maxPacketSize
|
||||
data:(NSData *)data {
|
||||
self = [super initWithPacketCounter:0];
|
||||
if (self) {
|
||||
_minVersion = minVersion;
|
||||
_maxVersion = maxVersion;
|
||||
_maxPacketSize = maxPacketSize;
|
||||
_data = data;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithPacketCounter:(UInt8)packetCounter {
|
||||
[self doesNotRecognizeSelector:_cmd];
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (BOOL)visitWithHandler:(id<GNSWeavePacketHandler>)handler context:(id)context {
|
||||
if ([handler respondsToSelector:@selector(handleConnectionRequestPacket:context:)]) {
|
||||
[handler handleConnectionRequestPacket:self context:context];
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (NSData *)serialize {
|
||||
NSMutableData *packet = [NSMutableData data];
|
||||
// The Weave protocol uses big-endian format (network byte order) for multi-byte types.
|
||||
UInt16 minVersionBigEndian = CFSwapInt16HostToBig(self.minVersion);
|
||||
UInt16 maxVersionBigEndian = CFSwapInt16HostToBig(self.maxVersion);
|
||||
UInt16 maxPacketSizeBigEndian = CFSwapInt16HostToBig(self.maxPacketSize);
|
||||
UInt8 header = WeaveControlPacketHeader(0, GNSWeaveControlCommandConnectionRequest);
|
||||
[packet appendBytes:&header length:sizeof(header)];
|
||||
[packet appendBytes:&minVersionBigEndian length:sizeof(minVersionBigEndian)];
|
||||
[packet appendBytes:&maxVersionBigEndian length:sizeof(maxVersionBigEndian)];
|
||||
[packet appendBytes:&maxPacketSizeBigEndian length:sizeof(maxPacketSizeBigEndian)];
|
||||
[packet appendData:self.data];
|
||||
NSAssert(packet.length <= kMaxControlPacketSize, @"Control packets cannot be larger than %lu",
|
||||
(unsigned long)kMaxControlPacketSize);
|
||||
return packet;
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
return [NSString stringWithFormat:@"<%@: %p, packet counter: %d, min version: %d, max version: "
|
||||
@"%d, max packet size: %d, data size: %ld>",
|
||||
[self class], self, self.packetCounter, _minVersion,
|
||||
_maxVersion, _maxPacketSize, (unsigned long)_data.length];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation GNSWeaveConnectionConfirmPacket
|
||||
|
||||
- (instancetype)initWithVersion:(UInt16)version packetSize:(UInt16)packetSize data:(NSData *)data {
|
||||
NSAssert(packetSize >= kGNSMinSupportedPacketSize, @"The minimum packet size is %ld",
|
||||
(long)kGNSMinSupportedPacketSize);
|
||||
self = [super initWithPacketCounter:0];
|
||||
if (self) {
|
||||
_version = version;
|
||||
_packetSize = packetSize;
|
||||
_data = data;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithPacketCounter:(UInt8)packetCounter {
|
||||
[self doesNotRecognizeSelector:_cmd];
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (BOOL)visitWithHandler:(id<GNSWeavePacketHandler>)handler context:(id)context {
|
||||
if ([handler respondsToSelector:@selector(handleConnectionConfirmPacket:context:)]) {
|
||||
[handler handleConnectionConfirmPacket:self context:context];
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (NSData *)serialize {
|
||||
NSMutableData *packet = [NSMutableData data];
|
||||
// The Weave protocol uses big-endian format (network byte order) encoding for multi-byte types.
|
||||
UInt16 versionBigEndian = CFSwapInt16HostToBig(self.version);
|
||||
UInt16 packetSizeBigEndian = CFSwapInt16HostToBig(self.packetSize);
|
||||
UInt8 header = WeaveControlPacketHeader(0, GNSWeaveControlCommandConnectionConfirm);
|
||||
[packet appendBytes:&header length:sizeof(header)];
|
||||
[packet appendBytes:&versionBigEndian length:sizeof(versionBigEndian)];
|
||||
[packet appendBytes:&packetSizeBigEndian length:sizeof(packetSizeBigEndian)];
|
||||
[packet appendData:self.data];
|
||||
NSAssert(packet.length <= kMaxControlPacketSize, @"Control packets cannot be larger than %lu",
|
||||
(unsigned long)kMaxControlPacketSize);
|
||||
return packet;
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
return
|
||||
[NSString stringWithFormat:
|
||||
@"<%@: %p, packet counter: %d, version: %d, packet size: %d, data size: %ld>",
|
||||
[self class], self, self.packetCounter, _version, _packetSize,
|
||||
(unsigned long)_data.length];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation GNSWeaveErrorPacket
|
||||
|
||||
- (instancetype)initWithPacketCounter:(UInt8)packetCounter {
|
||||
return [super initWithPacketCounter:packetCounter];
|
||||
}
|
||||
|
||||
- (BOOL)visitWithHandler:(id<GNSWeavePacketHandler>)handler context:(id)context {
|
||||
if ([handler respondsToSelector:@selector(handleErrorPacket:context:)]) {
|
||||
[handler handleErrorPacket:self context:context];
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (NSData *)serialize {
|
||||
NSMutableData *packet = [NSMutableData data];
|
||||
UInt8 header = WeaveControlPacketHeader(self.packetCounter, GNSWeaveControlCommandError);
|
||||
[packet appendBytes:&header length:sizeof(header)];
|
||||
return packet;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation GNSWeaveDataPacket
|
||||
|
||||
+ (nullable GNSWeaveDataPacket *)dataPacketWithPacketCounter:(UInt8)packetCounter
|
||||
packetSize:(UInt16)packetSize
|
||||
data:(NSData *)data
|
||||
offset:(NSUInteger *)outOffset {
|
||||
NSAssert(packetCounter < kGNSMaxPacketCounterValue, @"The packet has more than 3 bits.");
|
||||
NSAssert(data.length == 0 || *outOffset < data.length,
|
||||
@"The offset falls outside of the data range, data length: %ld, outOffset: %ld",
|
||||
(unsigned long)data.length, (unsigned long)*outOffset);
|
||||
|
||||
BOOL isFirstPacket = (*outOffset == 0);
|
||||
NSInteger dataSize = MIN(packetSize - kHeaderSize, data.length - *outOffset);
|
||||
BOOL isLastPacket = (data.length <= (*outOffset + dataSize));
|
||||
NSData *packetData = [data subdataWithRange:NSMakeRange(*outOffset, dataSize)];
|
||||
*outOffset = *outOffset + dataSize;
|
||||
|
||||
return [[GNSWeaveDataPacket alloc] initWithPacketCounter:packetCounter
|
||||
firstPacket:isFirstPacket
|
||||
lastPacket:isLastPacket
|
||||
data:packetData];
|
||||
}
|
||||
|
||||
- (instancetype)initWithPacketCounter:(UInt8)packetCounter
|
||||
firstPacket:(BOOL)isFirstPacket
|
||||
lastPacket:(BOOL)isLastPacket
|
||||
data:(nonnull NSData *)data {
|
||||
self = [super initWithPacketCounter:packetCounter];
|
||||
if (self) {
|
||||
_firstPacket = isFirstPacket;
|
||||
_lastPacket = isLastPacket;
|
||||
_data = data;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithPacketCounter:(UInt8)packetCounter {
|
||||
[self doesNotRecognizeSelector:_cmd];
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (BOOL)visitWithHandler:(id<GNSWeavePacketHandler>)handler context:(id)context {
|
||||
if ([handler respondsToSelector:@selector(handleDataPacket:context:)]) {
|
||||
[handler handleDataPacket:self context:context];
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (NSData *)serialize {
|
||||
UInt8 header = WeaveDataPacketHeader(self.packetCounter, self.isFirstPacket, self.isLastPacket);
|
||||
NSMutableData *packet = [NSMutableData data];
|
||||
[packet appendBytes:&header length:sizeof(header)];
|
||||
[packet appendData:self.data];
|
||||
return packet;
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
return [NSString
|
||||
stringWithFormat:
|
||||
@"<%@: %p, packet counter: %d, first packet: %@, last packet: %@, data length: %ld>",
|
||||
[self class], self, self.packetCounter, _firstPacket ? @"YES" : @"NO",
|
||||
_lastPacket ? @"YES" : @"NO", (unsigned long)_data.length];
|
||||
}
|
||||
|
||||
@end
|
||||
+299
@@ -0,0 +1,299 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/ble/Sockets/Source/Central/GNSCentralManager+Private.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/ble/Sockets/Source/Central/GNSCentralPeerManager+Private.h"
|
||||
#import "third_party/objective_c/ocmock/v3/Source/OCMock/OCMock.h"
|
||||
|
||||
@interface TestGNSCentralManager : GNSCentralManager
|
||||
@end
|
||||
|
||||
@implementation TestGNSCentralManager
|
||||
|
||||
+ (CBCentralManager *)centralManagerWithDelegate:(id<CBCentralManagerDelegate>)delegate
|
||||
queue:(dispatch_queue_t)queue
|
||||
options:(NSDictionary *)options {
|
||||
CBCentralManager *result = OCMStrictClassMock([CBCentralManager class]);
|
||||
OCMStub([result delegate]).andReturn(delegate);
|
||||
return result;
|
||||
}
|
||||
|
||||
- (GNSCentralPeerManager *)createCentralPeerManagerWithPeripheral:(CBPeripheral *)peripheral {
|
||||
GNSCentralPeerManager *manager = OCMStrictClassMock([GNSCentralPeerManager class]);
|
||||
OCMStub([manager cbPeripheral]).andReturn(peripheral);
|
||||
return manager;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface GNSCentralManagerTest : XCTestCase {
|
||||
CBUUID *_socketServiceUUID;
|
||||
TestGNSCentralManager *_centralManager;
|
||||
CBCentralManager *_cbCentralManagerMock;
|
||||
CBCentralManagerState _cbCentralManagerState;
|
||||
NSMutableArray *_mockObjectsToVerify;
|
||||
id<GNSCentralManagerDelegate> _centralManagerDelegate;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation GNSCentralManagerTest
|
||||
|
||||
- (void)setUp {
|
||||
_mockObjectsToVerify = [NSMutableArray array];
|
||||
_socketServiceUUID = [CBUUID UUIDWithNSUUID:[NSUUID UUID]];
|
||||
_centralManager = [[TestGNSCentralManager alloc]
|
||||
initWithSocketServiceUUID:_socketServiceUUID
|
||||
queue:dispatch_get_main_queue()];
|
||||
_centralManagerDelegate = OCMStrictProtocolMock(@protocol(GNSCentralManagerDelegate));
|
||||
_centralManager.delegate = _centralManagerDelegate;
|
||||
XCTAssertFalse(_centralManager.scanning);
|
||||
XCTAssertEqualObjects(_centralManager.socketServiceUUID, _socketServiceUUID);
|
||||
_cbCentralManagerMock = _centralManager.testing_cbCentralManager;
|
||||
XCTAssertEqual(_cbCentralManagerMock.delegate, _centralManager);
|
||||
_cbCentralManagerState = CBCentralManagerStatePoweredOff;
|
||||
OCMStub([_cbCentralManagerMock state])
|
||||
.andDo(^(NSInvocation *invocation) {
|
||||
[invocation setReturnValue:&_cbCentralManagerState];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)tearDown {
|
||||
OCMVerifyAll((id)_cbCentralManagerMock);
|
||||
OCMVerifyAll((id)_centralManagerDelegate);
|
||||
for (OCMockObject *object in _mockObjectsToVerify) {
|
||||
OCMVerifyAll(object);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Scanning And Power Off/On Bluetooth
|
||||
|
||||
- (void)startScanningWithServices:(NSArray *)services
|
||||
advertismentName:(NSString *)advertismentName {
|
||||
NSDictionary *options = @{ CBCentralManagerScanOptionAllowDuplicatesKey : @YES };
|
||||
OCMExpect([_cbCentralManagerMock scanForPeripheralsWithServices:services options:options]);
|
||||
[_centralManager startScanWithAdvertisedName:advertismentName
|
||||
advertisedServiceUUID:_centralManager.socketServiceUUID];
|
||||
XCTAssertTrue(_centralManager.scanning);
|
||||
}
|
||||
|
||||
- (void)testScanningWithBluetoothOFF {
|
||||
[_centralManager startScanWithAdvertisedName:nil
|
||||
advertisedServiceUUID:_centralManager.socketServiceUUID];
|
||||
XCTAssertTrue(_centralManager.scanning);
|
||||
[_centralManager stopScan];
|
||||
XCTAssertFalse(_centralManager.scanning);
|
||||
}
|
||||
|
||||
- (void)testScanningWithBluetoothON {
|
||||
_cbCentralManagerState = CBCentralManagerStatePoweredOn;
|
||||
NSArray *services = @[ _socketServiceUUID ];
|
||||
[self startScanningWithServices:services advertismentName:nil];
|
||||
OCMExpect([_cbCentralManagerMock stopScan]);
|
||||
[_centralManager stopScan];
|
||||
XCTAssertFalse(_centralManager.scanning);
|
||||
}
|
||||
|
||||
- (void)testScanningAndPowerONBluetooth {
|
||||
NSArray *services = @[ _socketServiceUUID ];
|
||||
[self startScanningWithServices:services advertismentName:nil];
|
||||
OCMExpect([_centralManagerDelegate centralManagerDidUpdateBLEState:_centralManager]);
|
||||
_cbCentralManagerState = CBCentralManagerStatePoweredOn;
|
||||
[_centralManager centralManagerDidUpdateState:_cbCentralManagerMock];
|
||||
XCTAssertTrue(_centralManager.scanning);
|
||||
OCMExpect([_cbCentralManagerMock stopScan]);
|
||||
[_centralManager stopScan];
|
||||
XCTAssertFalse(_centralManager.scanning);
|
||||
}
|
||||
|
||||
- (void)testScanningAndPowerOFFBluetooth {
|
||||
_cbCentralManagerState = CBCentralManagerStatePoweredOn;
|
||||
NSArray *services = @[ _socketServiceUUID ];
|
||||
[self startScanningWithServices:services advertismentName:nil];
|
||||
OCMExpect([_centralManagerDelegate centralManagerDidUpdateBLEState:_centralManager]);
|
||||
OCMExpect([_cbCentralManagerMock stopScan]);
|
||||
_cbCentralManagerState = CBCentralManagerStatePoweredOff;
|
||||
[_centralManager centralManagerDidUpdateState:_cbCentralManagerMock];
|
||||
XCTAssertTrue(_centralManager.scanning);
|
||||
[_centralManager stopScan];
|
||||
XCTAssertFalse(_centralManager.scanning);
|
||||
}
|
||||
|
||||
- (void)testScanningWithAdvertisedName {
|
||||
_cbCentralManagerState = CBCentralManagerStatePoweredOn;
|
||||
[self startScanningWithServices:nil advertismentName:@"advertisedname"];
|
||||
OCMExpect([_cbCentralManagerMock stopScan]);
|
||||
[_centralManager stopScan];
|
||||
XCTAssertFalse(_centralManager.scanning);
|
||||
}
|
||||
|
||||
#pragma mark - Scanning with no advertised name
|
||||
|
||||
- (void)testScanningPeripheralWithWrongPeripheralService {
|
||||
_cbCentralManagerState = CBCentralManagerStatePoweredOn;
|
||||
[self startScanningWithServices:@[ _socketServiceUUID ] advertismentName:nil];
|
||||
CBPeripheral *peripheral = OCMStrictClassMock([CBPeripheral class]);
|
||||
NSDictionary *advertisementData = @{ CBAdvertisementDataServiceUUIDsKey: @[ [NSUUID UUID] ] };
|
||||
[_centralManager centralManager:_cbCentralManagerMock
|
||||
didDiscoverPeripheral:peripheral
|
||||
advertisementData:advertisementData
|
||||
RSSI:@(19)];
|
||||
}
|
||||
|
||||
- (void)testScanningPeripheral {
|
||||
_cbCentralManagerState = CBCentralManagerStatePoweredOn;
|
||||
[self startScanningWithServices:@[ _socketServiceUUID ] advertismentName:nil];
|
||||
CBPeripheral *peripheral = OCMStrictClassMock([CBPeripheral class]);
|
||||
OCMStub([peripheral identifier]).andReturn([NSUUID UUID]);
|
||||
NSDictionary *advertisementData = @{
|
||||
CBAdvertisementDataServiceUUIDsKey : @[ _socketServiceUUID ]
|
||||
};
|
||||
OCMExpect([_centralManagerDelegate
|
||||
centralManager:_centralManager
|
||||
didDiscoverPeer:[OCMArg checkWithBlock:^BOOL(GNSCentralPeerManager *centralPeerManager) {
|
||||
XCTAssertEqual(centralPeerManager.cbPeripheral, peripheral);
|
||||
return YES;
|
||||
}]
|
||||
advertisementData:[OCMArg any]]);
|
||||
[_centralManager centralManager:_cbCentralManagerMock
|
||||
didDiscoverPeripheral:peripheral
|
||||
advertisementData:advertisementData
|
||||
RSSI:@(42)];
|
||||
}
|
||||
|
||||
#pragma mark - Scanning with advertised name
|
||||
|
||||
- (void)testScanningPeripheralWithWrongAdvertisedName {
|
||||
_cbCentralManagerState = CBCentralManagerStatePoweredOn;
|
||||
[self startScanningWithServices:nil advertismentName:@"advertisedname"];
|
||||
CBPeripheral *peripheral = OCMStrictClassMock([CBPeripheral class]);
|
||||
NSDictionary *advertisementData = @{
|
||||
CBAdvertisementDataServiceUUIDsKey : @[ [NSUUID UUID] ],
|
||||
CBAdvertisementDataLocalNameKey : @"wrongadvertisedname",
|
||||
};
|
||||
[_centralManager centralManager:_cbCentralManagerMock
|
||||
didDiscoverPeripheral:peripheral
|
||||
advertisementData:advertisementData
|
||||
RSSI:@(19)];
|
||||
}
|
||||
|
||||
- (void)testScanningPeripheralWithRightAdvertisedName {
|
||||
_cbCentralManagerState = CBCentralManagerStatePoweredOn;
|
||||
NSString *advertisedName = @"advertisedname";
|
||||
[self startScanningWithServices:nil advertismentName:advertisedName];
|
||||
CBPeripheral *peripheral = OCMStrictClassMock([CBPeripheral class]);
|
||||
OCMStub([peripheral identifier]).andReturn([NSUUID UUID]);
|
||||
NSDictionary *advertisementData = @{
|
||||
CBAdvertisementDataServiceUUIDsKey : @[ _socketServiceUUID ],
|
||||
CBAdvertisementDataLocalNameKey : advertisedName,
|
||||
};
|
||||
OCMExpect([_centralManagerDelegate centralManager:_centralManager
|
||||
didDiscoverPeer:[OCMArg any]
|
||||
advertisementData:[OCMArg any]]);
|
||||
[_centralManager centralManager:_cbCentralManagerMock
|
||||
didDiscoverPeripheral:peripheral
|
||||
advertisementData:advertisementData
|
||||
RSSI:@(19)];
|
||||
}
|
||||
|
||||
#pragma mark - Peripheral Retrieval
|
||||
|
||||
- (CBPeripheral *)prepareCBPeripheralWithIdentifier:(NSUUID *)identifier
|
||||
peripheralState:(CBPeripheralState *)peripheralState {
|
||||
CBPeripheral *peripheral = OCMStrictClassMock([CBPeripheral class]);
|
||||
OCMStub([peripheral identifier]).andReturn(identifier);
|
||||
OCMStub([peripheral state])
|
||||
.andDo(^(NSInvocation *invocation) {
|
||||
[invocation setReturnValue:peripheralState];
|
||||
});
|
||||
[_mockObjectsToVerify addObject:(OCMockObject *)peripheral];
|
||||
return peripheral;
|
||||
}
|
||||
|
||||
- (GNSCentralPeerManager *)retrieveCentralPeerWithPeripheral:(CBPeripheral *)peripheral {
|
||||
NSUUID *identifier = peripheral.identifier;
|
||||
OCMExpect([_cbCentralManagerMock retrievePeripheralsWithIdentifiers:@[ identifier ]])
|
||||
.andReturn([NSArray arrayWithObject:peripheral]);
|
||||
GNSCentralPeerManager *centralPeerManager =
|
||||
[_centralManager retrieveCentralPeerWithIdentifier:identifier];
|
||||
OCMStub([centralPeerManager cbPeripheral]).andReturn(peripheral);
|
||||
OCMStub([centralPeerManager identifier]).andReturn(identifier);
|
||||
XCTAssertNotNil(centralPeerManager);
|
||||
[_mockObjectsToVerify addObject:(OCMockObject *)centralPeerManager];
|
||||
return centralPeerManager;
|
||||
}
|
||||
|
||||
- (void)testRetrieveCentralPeerAndConnectDisconnect {
|
||||
_cbCentralManagerState = CBCentralManagerStatePoweredOn;
|
||||
NSUUID *identifier = [NSUUID UUID];
|
||||
CBPeripheralState peripheralMockState = CBPeripheralStateConnected;
|
||||
CBPeripheral *peripheral =
|
||||
[self prepareCBPeripheralWithIdentifier:identifier peripheralState:&peripheralMockState];
|
||||
GNSCentralPeerManager *centralPeerManager = [self retrieveCentralPeerWithPeripheral:peripheral];
|
||||
OCMExpect([centralPeerManager bleConnected]);
|
||||
[_centralManager centralManager:_cbCentralManagerMock didConnectPeripheral:peripheral];
|
||||
NSError *error = [NSError errorWithDomain:@"test" code:1 userInfo:nil];
|
||||
OCMExpect([centralPeerManager bleDisconnectedWithError:error]);
|
||||
peripheralMockState = CBPeripheralStateDisconnected;
|
||||
[_centralManager centralManager:_cbCentralManagerMock
|
||||
didDisconnectPeripheral:peripheral
|
||||
error:error];
|
||||
}
|
||||
|
||||
- (void)testRetrieveTwiceCentralPeer {
|
||||
_cbCentralManagerState = CBCentralManagerStatePoweredOn;
|
||||
NSUUID *identifier = [NSUUID UUID];
|
||||
CBPeripheralState peripheralMockState = CBPeripheralStateConnected;
|
||||
CBPeripheral *peripheral =
|
||||
[self prepareCBPeripheralWithIdentifier:identifier peripheralState:&peripheralMockState];
|
||||
GNSCentralPeerManager *centralPeerManager = [self retrieveCentralPeerWithPeripheral:peripheral];
|
||||
XCTAssertNil([_centralManager retrieveCentralPeerWithIdentifier:identifier]);
|
||||
OCMExpect([centralPeerManager bleConnected]);
|
||||
[_centralManager centralManager:_cbCentralManagerMock didConnectPeripheral:peripheral];
|
||||
NSError *error = [NSError errorWithDomain:@"test" code:1 userInfo:nil];
|
||||
OCMExpect([centralPeerManager bleDisconnectedWithError:error]);
|
||||
peripheralMockState = CBPeripheralStateDisconnected;
|
||||
[_centralManager centralManager:_cbCentralManagerMock
|
||||
didDisconnectPeripheral:peripheral
|
||||
error:error];
|
||||
[_centralManager centralPeerManagerDidDisconnect:centralPeerManager];
|
||||
|
||||
NSUUID *identifier2 = [NSUUID UUID];
|
||||
CBPeripheralState peripheralState2 = CBPeripheralStateConnected;
|
||||
CBPeripheral *peripheral2 =
|
||||
[self prepareCBPeripheralWithIdentifier:identifier2 peripheralState:&peripheralState2];
|
||||
GNSCentralPeerManager *centralPeerManager2 = [self retrieveCentralPeerWithPeripheral:peripheral2];
|
||||
XCTAssertNotNil(centralPeerManager2);
|
||||
XCTAssertNotEqual(centralPeerManager, centralPeerManager2);
|
||||
}
|
||||
|
||||
- (void)testRetrieveCentralPeerAndConnectFailToConnect {
|
||||
_cbCentralManagerState = CBCentralManagerStatePoweredOn;
|
||||
NSUUID *identifier = [NSUUID UUID];
|
||||
CBPeripheralState peripheralMockState = CBPeripheralStateConnected;
|
||||
CBPeripheral *peripheral =
|
||||
[self prepareCBPeripheralWithIdentifier:identifier peripheralState:&peripheralMockState];
|
||||
GNSCentralPeerManager *centralPeerManager = [self retrieveCentralPeerWithPeripheral:peripheral];
|
||||
OCMExpect([centralPeerManager bleConnected]);
|
||||
[_centralManager centralManager:_cbCentralManagerMock didConnectPeripheral:peripheral];
|
||||
NSError *error = [NSError errorWithDomain:@"test" code:1 userInfo:nil];
|
||||
OCMExpect([centralPeerManager bleDisconnectedWithError:error]);
|
||||
peripheralMockState = CBPeripheralStateDisconnected;
|
||||
[_centralManager centralManager:_cbCentralManagerMock
|
||||
didFailToConnectPeripheral:peripheral
|
||||
error:error];
|
||||
}
|
||||
|
||||
@end
|
||||
+465
@@ -0,0 +1,465 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/ble/Sockets/Source/Central/GNSCentralManager+Private.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/ble/Sockets/Source/Central/GNSCentralPeerManager+Private.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/ble/Sockets/Source/Shared/GNSWeavePacket.h"
|
||||
#import "third_party/objective_c/ocmock/v3/Source/OCMock/OCMock.h"
|
||||
|
||||
static SEL gTimerSelector = nil;
|
||||
static GNSCentralPeerManager *gTimerTarget = nil;
|
||||
|
||||
static void ClearTimer(void) {
|
||||
gTimerSelector = nil;
|
||||
gTimerTarget = nil;
|
||||
}
|
||||
|
||||
static void FireTimer(void) {
|
||||
NSCAssert(gTimerTarget != nil, @"The timer target cannot be nil.");
|
||||
NSCAssert(gTimerSelector != nil, @"The timer selector cannot be nil.");
|
||||
NSInvocation *invocation = [NSInvocation
|
||||
invocationWithMethodSignature:[[gTimerTarget class]
|
||||
instanceMethodSignatureForSelector:gTimerSelector]];
|
||||
invocation.target = gTimerTarget;
|
||||
invocation.selector = gTimerSelector;
|
||||
[invocation invoke];
|
||||
ClearTimer();
|
||||
}
|
||||
|
||||
@interface TestGNSCentralPeerManager : GNSCentralPeerManager
|
||||
@end
|
||||
|
||||
@implementation TestGNSCentralPeerManager
|
||||
|
||||
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval
|
||||
target:(id)target
|
||||
selector:(SEL)selector
|
||||
userInfo:(nullable id)userInfo
|
||||
repeats:(BOOL)yesOrNo {
|
||||
NSAssert(target != nil, @"The timer target cannot be nil.");
|
||||
NSAssert(selector != nil, @"The timer selector cannot be nil.");
|
||||
NSAssert(gTimerSelector == nil, @"Timer selector already set.");
|
||||
NSAssert(gTimerTarget == nil, @"Time target already set.");
|
||||
gTimerSelector = selector;
|
||||
gTimerTarget = target;
|
||||
NSTimer *timer = OCMStrictClassMock([NSTimer class]);
|
||||
OCMStub([timer timeInterval]).andReturn(timeInterval);
|
||||
return timer;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface GNSCentralPeerManagerTest : XCTestCase {
|
||||
TestGNSCentralPeerManager *_centralPeerManager;
|
||||
CBCentralManagerState _cbCentralManagerState;
|
||||
GNSCentralManager *_centralManagerMock;
|
||||
CBUUID *_serviceUUID;
|
||||
CBPeripheral *_peripheralMock;
|
||||
CBPeripheralState _peripheralState;
|
||||
NSUUID *_peripheralUUID;
|
||||
CBCharacteristic *_toPeripheralCharacteristic;
|
||||
CBCharacteristic *_fromPeripheralCharacteristic;
|
||||
CBCharacteristic *_pairingCharacteristic;
|
||||
NSArray *_characteristics;
|
||||
NSArray *_characteristicsWithPairing;
|
||||
CBService *_serviceMock;
|
||||
NSArray *_services;
|
||||
GNSSocket *_socket;
|
||||
id<GNSSocketDelegate> _socketDelegateMock;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation GNSCentralPeerManagerTest
|
||||
|
||||
- (void)setUp {
|
||||
_peripheralUUID = [NSUUID UUID];
|
||||
_serviceUUID = [CBUUID UUIDWithNSUUID:[NSUUID UUID]];
|
||||
_centralManagerMock = [OCMStrictClassMock([GNSCentralManager class]) noRetainObjectArgs];
|
||||
OCMStub([_centralManagerMock socketServiceUUID]).andReturn(_serviceUUID);
|
||||
_cbCentralManagerState = CBCentralManagerStatePoweredOn;
|
||||
OCMStub([_centralManagerMock cbCentralManagerState])
|
||||
.andDo(^(NSInvocation *invocation) {
|
||||
[invocation setReturnValue:&_cbCentralManagerState];
|
||||
});
|
||||
_peripheralMock = [OCMStrictClassMock([CBPeripheral class]) noRetainObjectArgs];
|
||||
OCMStub([_peripheralMock identifier]).andReturn(_peripheralUUID);
|
||||
_peripheralState = CBPeripheralStateDisconnected;
|
||||
OCMStub([_peripheralMock state])
|
||||
.andDo(^(NSInvocation *invocation) {
|
||||
[invocation setReturnValue:&_peripheralState];
|
||||
});
|
||||
OCMExpect([_peripheralMock setDelegate:[OCMArg isNotNil]]);
|
||||
_centralPeerManager = [[TestGNSCentralPeerManager alloc] initWithPeripheral:_peripheralMock
|
||||
centralManager:_centralManagerMock];
|
||||
void *centralPeerManagerNonRetained = (__bridge void *)(_centralPeerManager);
|
||||
OCMStub([_peripheralMock delegate]).andReturn(centralPeerManagerNonRetained);
|
||||
_toPeripheralCharacteristic =
|
||||
[self generateCharateristicMockWithIdentifierString:@"00000100-0004-1000-8000-001A11000101"];
|
||||
_fromPeripheralCharacteristic =
|
||||
[self generateCharateristicMockWithIdentifierString:@"00000100-0004-1000-8000-001A11000102"];
|
||||
_pairingCharacteristic =
|
||||
[self generateCharateristicMockWithIdentifierString:@"17836FBD-8C6A-4B81-83CE-8560629E834B"];
|
||||
_characteristics = @[ _toPeripheralCharacteristic, _fromPeripheralCharacteristic ];
|
||||
_characteristicsWithPairing =
|
||||
@[ _toPeripheralCharacteristic, _fromPeripheralCharacteristic, _pairingCharacteristic ];
|
||||
_serviceMock = OCMStrictClassMock([CBService class]);
|
||||
OCMStub([_serviceMock UUID]).andReturn(_serviceUUID);
|
||||
_services = @[ _serviceMock ];
|
||||
OCMStub([_peripheralMock services]).andReturn(_services);
|
||||
_socketDelegateMock = OCMStrictProtocolMock(@protocol(GNSSocketDelegate));
|
||||
}
|
||||
|
||||
- (void)tearDown {
|
||||
if (_peripheralState != CBPeripheralStateDisconnected) {
|
||||
// CBPeripharal must be disconnected when GNSCentralPeerManager is dealocated. OCM retains the
|
||||
// parameter. So |_centralManagerMock| cannot be set in the paramter in order to be deallocated
|
||||
// correctly.
|
||||
void *centralPeerManagerNonRetained = (__bridge void *)(_centralPeerManager);
|
||||
OCMExpect([_centralManagerMock
|
||||
cancelPeripheralConnectionForPeer:[OCMArg checkWithBlock:^BOOL(id obj) {
|
||||
return obj == centralPeerManagerNonRetained;
|
||||
}]]);
|
||||
}
|
||||
_centralPeerManager = nil;
|
||||
ClearTimer();
|
||||
OCMVerifyAll((id)_centralManagerMock);
|
||||
OCMVerifyAll((id)_peripheralMock);
|
||||
OCMVerifyAll((id)_toPeripheralCharacteristic);
|
||||
OCMVerifyAll((id)_fromPeripheralCharacteristic);
|
||||
OCMVerifyAll((id)_pairingCharacteristic);
|
||||
OCMVerifyAll((id)_serviceMock);
|
||||
OCMVerifyAll((id)_socketDelegateMock);
|
||||
__weak TestGNSCentralPeerManager *weakCentralPeerManager = _centralPeerManager;
|
||||
_centralPeerManager = nil;
|
||||
XCTAssertNil(weakCentralPeerManager);
|
||||
}
|
||||
|
||||
- (NSDictionary *)peripheralConnectionOptions {
|
||||
return @{
|
||||
CBConnectPeripheralOptionNotifyOnDisconnectionKey : @YES,
|
||||
#if TARGET_OS_IPHONE
|
||||
CBConnectPeripheralOptionNotifyOnConnectionKey : @YES,
|
||||
CBConnectPeripheralOptionNotifyOnNotificationKey : @YES,
|
||||
#endif
|
||||
};
|
||||
}
|
||||
|
||||
- (NSSet *)characteristicUUIDSetWithPairingUUID:(BOOL)pairing {
|
||||
NSMutableSet *uuidSet = [NSMutableSet set];
|
||||
[uuidSet addObject:_toPeripheralCharacteristic.UUID];
|
||||
[uuidSet addObject:_fromPeripheralCharacteristic.UUID];
|
||||
if (pairing) {
|
||||
[uuidSet addObject:_pairingCharacteristic.UUID];
|
||||
}
|
||||
return uuidSet;
|
||||
}
|
||||
|
||||
- (void)checkCharacteristicsUUID:(NSArray *)uuids withPairingUUID:(BOOL)pairing {
|
||||
NSSet *uuidSet = [NSSet setWithArray:uuids];
|
||||
XCTAssertEqualObjects(uuidSet, [self characteristicUUIDSetWithPairingUUID:pairing]);
|
||||
}
|
||||
|
||||
- (CBCharacteristic *)generateCharateristicMockWithIdentifierString:(NSString *)identifierString {
|
||||
CBCharacteristic *characteristic = OCMStrictClassMock([CBCharacteristic class]);
|
||||
CBUUID *toPeripheralCharUUID = [CBUUID UUIDWithString:identifierString];
|
||||
OCMStub([characteristic UUID]).andReturn(toPeripheralCharUUID);
|
||||
return characteristic;
|
||||
}
|
||||
|
||||
- (void)transitionToSocketCommunicationStateWithPairingChar:(BOOL)hasPairingChar {
|
||||
OCMExpect([_centralManagerMock connectPeripheralForPeer:_centralPeerManager
|
||||
options:[self peripheralConnectionOptions]]);
|
||||
[_centralPeerManager socketWithPairingCharacteristic:hasPairingChar
|
||||
completion:^(GNSSocket *newSocket, NSError *error) {
|
||||
XCTAssertNil(error);
|
||||
XCTAssertNotNil(newSocket);
|
||||
XCTAssertNil(_socket);
|
||||
_socket = newSocket;
|
||||
}];
|
||||
OCMExpect([_peripheralMock discoverServices:@[ _serviceUUID ]]);
|
||||
_peripheralState = CBPeripheralStateConnected;
|
||||
[_centralPeerManager bleConnected];
|
||||
OCMExpect([_peripheralMock discoverCharacteristics:[OCMArg checkWithBlock:^BOOL(id obj) {
|
||||
[self checkCharacteristicsUUID:obj withPairingUUID:hasPairingChar];
|
||||
return YES;
|
||||
}]
|
||||
forService:_serviceMock]);
|
||||
[_centralPeerManager peripheral:_peripheralMock didDiscoverServices:nil];
|
||||
OCMStub([_serviceMock characteristics]).andReturn(_characteristicsWithPairing);
|
||||
OCMExpect([_peripheralMock setNotifyValue:YES forCharacteristic:_fromPeripheralCharacteristic]);
|
||||
[_centralPeerManager peripheral:_peripheralMock
|
||||
didDiscoverCharacteristicsForService:_serviceMock
|
||||
error:nil];
|
||||
GNSWeaveConnectionRequestPacket *connectionRequest =
|
||||
[[GNSWeaveConnectionRequestPacket alloc] initWithMinVersion:1
|
||||
maxVersion:1
|
||||
maxPacketSize:0
|
||||
data:nil];
|
||||
OCMExpect([_peripheralMock writeValue:[connectionRequest serialize]
|
||||
forCharacteristic:_toPeripheralCharacteristic
|
||||
type:CBCharacteristicWriteWithResponse]);
|
||||
[_centralPeerManager peripheral:_peripheralMock
|
||||
didUpdateNotificationStateForCharacteristic:_fromPeripheralCharacteristic
|
||||
error:nil];
|
||||
XCTAssertNotNil(_socket);
|
||||
_socket.delegate = _socketDelegateMock;
|
||||
}
|
||||
|
||||
- (void)simulateConnectedSocketWithPairingChar:(BOOL)hasPairingChar {
|
||||
[self transitionToSocketCommunicationStateWithPairingChar:hasPairingChar];
|
||||
OCMExpect([_socketDelegateMock socketDidConnect:_socket]);
|
||||
GNSWeaveConnectionConfirmPacket *confirmConnection =
|
||||
[[GNSWeaveConnectionConfirmPacket alloc] initWithVersion:1 packetSize:100 data:nil];
|
||||
OCMExpect([_fromPeripheralCharacteristic value]).andReturn([confirmConnection serialize]);
|
||||
OCMExpect([_centralPeerManager.testing_connectionConfirmTimer invalidate]);
|
||||
[_centralPeerManager peripheral:_peripheralMock
|
||||
didUpdateValueForCharacteristic:_fromPeripheralCharacteristic
|
||||
error:nil];
|
||||
// When the connection confirm packet is received the socket should be invalidated and released.
|
||||
// It's important to call |ClearTimer| here, since it retains |_centralPeerManager|.
|
||||
XCTAssertNil(_centralPeerManager.testing_connectionConfirmTimer);
|
||||
ClearTimer();
|
||||
}
|
||||
|
||||
- (void)testCentralPeerManager {
|
||||
XCTAssertEqualObjects(_centralPeerManager.identifier, _peripheralUUID);
|
||||
// The dealloc should set the delegate to nil.
|
||||
OCMExpect([_peripheralMock setDelegate:nil]);
|
||||
}
|
||||
|
||||
- (void)testGetSocket {
|
||||
[self simulateConnectedSocketWithPairingChar:NO];
|
||||
XCTAssertNotNil(_socket);
|
||||
// The dealloc should set the delegate to nil.
|
||||
OCMExpect([_peripheralMock setDelegate:nil]);
|
||||
}
|
||||
|
||||
- (void)testBLEDisconnectBeforeConnect {
|
||||
OCMExpect([_centralManagerMock connectPeripheralForPeer:_centralPeerManager
|
||||
options:[self peripheralConnectionOptions]]);
|
||||
[_centralPeerManager socketWithPairingCharacteristic:NO
|
||||
completion:^(GNSSocket *socket, NSError *error) {
|
||||
XCTAssertNil(socket);
|
||||
XCTAssertEqual(error.code, GNSErrorNoConnection);
|
||||
}];
|
||||
_peripheralState = CBPeripheralStateDisconnected;
|
||||
OCMExpect([_peripheralMock setDelegate:nil]);
|
||||
OCMExpect([_centralManagerMock centralPeerManagerDidDisconnect:_centralPeerManager]);
|
||||
[_centralPeerManager bleDisconnectedWithError:nil];
|
||||
}
|
||||
|
||||
- (void)testBLEDisconnectAfterConnect {
|
||||
NSError *bleDisconnectError = [NSError errorWithDomain:@"test" code:1 userInfo:nil];
|
||||
OCMExpect([_centralManagerMock connectPeripheralForPeer:_centralPeerManager
|
||||
options:[self peripheralConnectionOptions]]);
|
||||
[_centralPeerManager socketWithPairingCharacteristic:NO
|
||||
completion:^(GNSSocket *socket, NSError *error) {
|
||||
XCTAssertEqual(error, bleDisconnectError);
|
||||
XCTAssertNil(socket);
|
||||
}];
|
||||
OCMExpect([_peripheralMock discoverServices:@[ _serviceUUID ]]);
|
||||
_peripheralState = CBPeripheralStateConnected;
|
||||
[_centralPeerManager bleConnected];
|
||||
_peripheralState = CBPeripheralStateDisconnected;
|
||||
OCMExpect([_peripheralMock setDelegate:nil]);
|
||||
OCMExpect([_centralManagerMock centralPeerManagerDidDisconnect:_centralPeerManager]);
|
||||
[_centralPeerManager bleDisconnectedWithError:bleDisconnectError];
|
||||
}
|
||||
|
||||
- (void)testDiscoverServiceError {
|
||||
NSError *discoverServiceError = [NSError errorWithDomain:@"test" code:1 userInfo:nil];
|
||||
OCMExpect([_centralManagerMock connectPeripheralForPeer:_centralPeerManager
|
||||
options:[self peripheralConnectionOptions]]);
|
||||
[_centralPeerManager socketWithPairingCharacteristic:NO
|
||||
completion:^(GNSSocket *socket, NSError *error) {
|
||||
XCTAssertEqual(error, discoverServiceError);
|
||||
XCTAssertNil(socket);
|
||||
}];
|
||||
OCMExpect([_peripheralMock discoverServices:@[ _serviceUUID ]]);
|
||||
_peripheralState = CBPeripheralStateConnected;
|
||||
[_centralPeerManager bleConnected];
|
||||
OCMExpect([_centralManagerMock cancelPeripheralConnectionForPeer:_centralPeerManager]);
|
||||
[_centralPeerManager peripheral:_peripheralMock didDiscoverServices:discoverServiceError];
|
||||
_peripheralState = CBPeripheralStateDisconnected;
|
||||
OCMExpect([_peripheralMock setDelegate:nil]);
|
||||
OCMExpect([_centralManagerMock centralPeerManagerDidDisconnect:_centralPeerManager]);
|
||||
[_centralPeerManager bleDisconnectedWithError:nil];
|
||||
}
|
||||
|
||||
- (void)testDiscoverCharacteristicError {
|
||||
NSError *discoverCharacteristicError = [NSError errorWithDomain:@"test" code:1 userInfo:nil];
|
||||
OCMExpect([_centralManagerMock connectPeripheralForPeer:_centralPeerManager
|
||||
options:[self peripheralConnectionOptions]]);
|
||||
[_centralPeerManager socketWithPairingCharacteristic:NO
|
||||
completion:^(GNSSocket *socket, NSError *error) {
|
||||
XCTAssertEqual(error, discoverCharacteristicError);
|
||||
XCTAssertNil(socket);
|
||||
}];
|
||||
OCMExpect([_peripheralMock discoverServices:@[ _serviceUUID ]]);
|
||||
_peripheralState = CBPeripheralStateConnected;
|
||||
[_centralPeerManager bleConnected];
|
||||
OCMExpect([_peripheralMock discoverCharacteristics:[OCMArg checkWithBlock:^BOOL(id obj) {
|
||||
[self checkCharacteristicsUUID:obj withPairingUUID:NO];
|
||||
return YES;
|
||||
}]
|
||||
forService:_serviceMock]);
|
||||
[_centralPeerManager peripheral:_peripheralMock didDiscoverServices:nil];
|
||||
OCMExpect([_centralManagerMock cancelPeripheralConnectionForPeer:_centralPeerManager]);
|
||||
OCMStub([_serviceMock characteristics]).andReturn(_characteristics);
|
||||
[_centralPeerManager peripheral:_peripheralMock
|
||||
didDiscoverCharacteristicsForService:_serviceMock
|
||||
error:discoverCharacteristicError];
|
||||
_peripheralState = CBPeripheralStateDisconnected;
|
||||
OCMExpect([_peripheralMock setDelegate:nil]);
|
||||
OCMExpect([_centralManagerMock centralPeerManagerDidDisconnect:_centralPeerManager]);
|
||||
[_centralPeerManager bleDisconnectedWithError:nil];
|
||||
}
|
||||
|
||||
- (void)testSetNotifyValueForCharacteristicError {
|
||||
NSError *notificationError = [NSError errorWithDomain:@"test" code:1 userInfo:nil];
|
||||
OCMExpect([_centralManagerMock connectPeripheralForPeer:_centralPeerManager
|
||||
options:[self peripheralConnectionOptions]]);
|
||||
[_centralPeerManager socketWithPairingCharacteristic:NO
|
||||
completion:^(GNSSocket *socket, NSError *error) {
|
||||
XCTAssertEqual(error, notificationError);
|
||||
XCTAssertNil(socket);
|
||||
}];
|
||||
OCMExpect([_peripheralMock discoverServices:@[ _serviceUUID ]]);
|
||||
_peripheralState = CBPeripheralStateConnected;
|
||||
[_centralPeerManager bleConnected];
|
||||
OCMExpect([_peripheralMock discoverCharacteristics:[OCMArg checkWithBlock:^BOOL(id obj) {
|
||||
[self checkCharacteristicsUUID:obj withPairingUUID:NO];
|
||||
return YES;
|
||||
}]
|
||||
forService:_serviceMock]);
|
||||
[_centralPeerManager peripheral:_peripheralMock didDiscoverServices:nil];
|
||||
OCMExpect([_centralManagerMock cancelPeripheralConnectionForPeer:_centralPeerManager]);
|
||||
OCMStub([_serviceMock characteristics]).andReturn(_characteristics);
|
||||
OCMExpect([_peripheralMock setNotifyValue:YES forCharacteristic:_fromPeripheralCharacteristic]);
|
||||
[_centralPeerManager peripheral:_peripheralMock
|
||||
didDiscoverCharacteristicsForService:_serviceMock
|
||||
error:nil];
|
||||
[_centralPeerManager peripheral:_peripheralMock
|
||||
didUpdateNotificationStateForCharacteristic:_fromPeripheralCharacteristic
|
||||
error:notificationError];
|
||||
_peripheralState = CBPeripheralStateDisconnected;
|
||||
OCMExpect([_peripheralMock setDelegate:nil]);
|
||||
OCMExpect([_centralManagerMock centralPeerManagerDidDisconnect:_centralPeerManager]);
|
||||
[_centralPeerManager bleDisconnectedWithError:nil];
|
||||
}
|
||||
|
||||
- (void)testTimeOutWaitingForConnectionResponse {
|
||||
[self transitionToSocketCommunicationStateWithPairingChar:NO];
|
||||
OCMExpect([_centralManagerMock cancelPeripheralConnectionForPeer:_centralPeerManager]);
|
||||
FireTimer();
|
||||
_peripheralState = CBPeripheralStateDisconnected;
|
||||
OCMExpect([_peripheralMock setDelegate:nil]);
|
||||
OCMExpect([_centralManagerMock centralPeerManagerDidDisconnect:_centralPeerManager]);
|
||||
OCMExpect([_socketDelegateMock socket:_socket
|
||||
didDisconnectWithError:[OCMArg checkWithBlock:^BOOL(NSError *error) {
|
||||
return [error.domain isEqualToString:kGNSSocketsErrorDomain] &&
|
||||
error.code == GNSErrorConnectionTimedOut;
|
||||
}]]);
|
||||
[_centralPeerManager bleDisconnectedWithError:nil];
|
||||
XCTAssertNil(_centralPeerManager.testing_connectionConfirmTimer);
|
||||
}
|
||||
|
||||
- (void)testBLEDisconnectedWhileWaitingForConnectionResponse {
|
||||
[self transitionToSocketCommunicationStateWithPairingChar:NO];
|
||||
NSError *bleDisconnectError = [NSError errorWithDomain:@"test" code:1 userInfo:nil];
|
||||
_peripheralState = CBPeripheralStateDisconnected;
|
||||
OCMExpect([_peripheralMock setDelegate:nil]);
|
||||
OCMExpect([_centralManagerMock centralPeerManagerDidDisconnect:_centralPeerManager]);
|
||||
OCMExpect([_socketDelegateMock socket:_socket didDisconnectWithError:bleDisconnectError]);
|
||||
OCMExpect([_centralPeerManager.testing_connectionConfirmTimer invalidate]);
|
||||
[_centralPeerManager bleDisconnectedWithError:bleDisconnectError];
|
||||
XCTAssertNil(_centralPeerManager.testing_connectionConfirmTimer);
|
||||
}
|
||||
|
||||
- (void)testCancelPendingSocket {
|
||||
OCMExpect([_centralManagerMock connectPeripheralForPeer:_centralPeerManager
|
||||
options:[self peripheralConnectionOptions]]);
|
||||
[_centralPeerManager
|
||||
socketWithPairingCharacteristic:NO
|
||||
completion:^(GNSSocket *socket, NSError *error) {
|
||||
XCTAssertNil(socket);
|
||||
XCTAssertNotNil(error);
|
||||
XCTAssertEqualObjects(error.domain, kGNSSocketsErrorDomain);
|
||||
XCTAssertEqual(error.code, GNSErrorCancelPendingSocketRequested);
|
||||
}];
|
||||
OCMExpect([_peripheralMock discoverServices:@[ _serviceUUID ]]);
|
||||
_peripheralState = CBPeripheralStateConnected;
|
||||
[_centralPeerManager bleConnected];
|
||||
OCMExpect([_peripheralMock discoverCharacteristics:[OCMArg checkWithBlock:^BOOL(id obj) {
|
||||
[self checkCharacteristicsUUID:obj withPairingUUID:NO];
|
||||
return YES;
|
||||
}]
|
||||
forService:_serviceMock]);
|
||||
[_centralPeerManager peripheral:_peripheralMock didDiscoverServices:nil];
|
||||
OCMExpect([_centralManagerMock cancelPeripheralConnectionForPeer:_centralPeerManager]);
|
||||
[_centralPeerManager cancelPendingSocket];
|
||||
_peripheralState = CBPeripheralStateDisconnected;
|
||||
OCMExpect([_peripheralMock setDelegate:nil]);
|
||||
OCMExpect([_centralManagerMock centralPeerManagerDidDisconnect:_centralPeerManager]);
|
||||
[_centralPeerManager bleDisconnectedWithError:nil];
|
||||
}
|
||||
|
||||
- (void)testPairing {
|
||||
[self simulateConnectedSocketWithPairingChar:YES];
|
||||
XCTAssertNotNil(_socket);
|
||||
OCMExpect([_peripheralMock readValueForCharacteristic:_pairingCharacteristic]);
|
||||
[_centralPeerManager startBluetoothPairingWithCompletion:^(BOOL pairing, NSError *error) {
|
||||
XCTAssertTrue(pairing);
|
||||
XCTAssertNil(error);
|
||||
}];
|
||||
// The dealloc should set the delegate to nil.
|
||||
OCMExpect([_peripheralMock setDelegate:nil]);
|
||||
}
|
||||
|
||||
- (void)testDisconnect {
|
||||
[self simulateConnectedSocketWithPairingChar:NO];
|
||||
OCMExpect([_centralManagerMock cancelPeripheralConnectionForPeer:_centralPeerManager]);
|
||||
[_socket disconnect];
|
||||
_peripheralState = CBPeripheralStateDisconnected;
|
||||
OCMExpect([_peripheralMock setDelegate:nil]);
|
||||
OCMExpect([_centralManagerMock centralPeerManagerDidDisconnect:_centralPeerManager]);
|
||||
OCMExpect([_socketDelegateMock socket:_socket didDisconnectWithError:nil]);
|
||||
[_centralPeerManager bleDisconnectedWithError:nil];
|
||||
}
|
||||
|
||||
- (void)testDisconnectWithError {
|
||||
[self simulateConnectedSocketWithPairingChar:NO];
|
||||
OCMExpect([_centralManagerMock cancelPeripheralConnectionForPeer:_centralPeerManager]);
|
||||
[_socket disconnect];
|
||||
NSError *errorWhileBLEDisconnecting =
|
||||
[[NSError alloc] initWithDomain:@"domain" code:-42 userInfo:nil];
|
||||
_peripheralState = CBPeripheralStateDisconnected;
|
||||
OCMExpect([_peripheralMock setDelegate:nil]);
|
||||
OCMExpect([_centralManagerMock centralPeerManagerDidDisconnect:_centralPeerManager]);
|
||||
OCMExpect([_socketDelegateMock socket:_socket didDisconnectWithError:errorWhileBLEDisconnecting]);
|
||||
[_centralPeerManager bleDisconnectedWithError:errorWhileBLEDisconnecting];
|
||||
OCMVerifyAll((id)_socketDelegateMock);
|
||||
}
|
||||
|
||||
- (void)testDropSocketWhileConnecting {
|
||||
[self transitionToSocketCommunicationStateWithPairingChar:NO];
|
||||
OCMExpect([_centralManagerMock cancelPeripheralConnectionForPeer:_centralPeerManager]);
|
||||
OCMExpect([_centralPeerManager.testing_connectionConfirmTimer invalidate]);
|
||||
OCMExpect([_peripheralMock setDelegate:nil]);
|
||||
_socket = nil;
|
||||
}
|
||||
|
||||
@end
|
||||
+836
@@ -0,0 +1,836 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/ble/Sockets/Source/Peripheral/GNSPeripheralManager+Private.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/ble/Sockets/Source/Peripheral/GNSPeripheralServiceManager+Private.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/ble/Sockets/Source/Shared/GNSSocket+Private.h"
|
||||
#import "third_party/objective_c/ocmock/v3/Source/OCMock/OCMock.h"
|
||||
|
||||
@interface TestGNSPeripheralManager : GNSPeripheralManager
|
||||
@property(nonatomic, strong) id cbPeripheralManagerMock;
|
||||
@property(nonatomic, strong) NSDictionary *cbOptions;
|
||||
@end
|
||||
|
||||
@implementation TestGNSPeripheralManager
|
||||
|
||||
- (CBPeripheralManager *)cbPeripheralManagerWithDelegate:(id<CBPeripheralManagerDelegate>)delegate
|
||||
queue:(dispatch_queue_t)queue
|
||||
options:(NSDictionary *)options {
|
||||
_cbOptions = options;
|
||||
return _cbPeripheralManagerMock;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface GNSPeripheralManagerTest : XCTestCase {
|
||||
TestGNSPeripheralManager *_peripheralManager;
|
||||
NSString *_displayName;
|
||||
NSString *_restoreIdentifier;
|
||||
id _cbPeripheralManagerMock;
|
||||
NSDictionary *_cbAdvertisementData;
|
||||
CBPeripheralManagerState _cbPeripheralManagerState;
|
||||
NSMutableArray *_mocksToVerify;
|
||||
NSMutableDictionary *_cbServiceStatePerPeripheral;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation GNSPeripheralManagerTest
|
||||
|
||||
- (void)setUp {
|
||||
_mocksToVerify = [NSMutableArray array];
|
||||
_cbServiceStatePerPeripheral = [NSMutableDictionary dictionary];
|
||||
_displayName = @"DisplayName";
|
||||
_restoreIdentifier = @"RestoreIdentifier";
|
||||
_cbPeripheralManagerMock = OCMStrictClassMock([CBPeripheralManager class]);
|
||||
[_mocksToVerify addObject:_cbPeripheralManagerMock];
|
||||
OCMStub([_cbPeripheralManagerMock state]).andDo(^(NSInvocation *invocation) {
|
||||
[invocation setReturnValue:&_cbPeripheralManagerState];
|
||||
});
|
||||
OCMStub([_cbPeripheralManagerMock isAdvertising]).andDo(^(NSInvocation *invocation) {
|
||||
BOOL isAdvertising = _cbAdvertisementData != nil;
|
||||
[invocation setReturnValue:&isAdvertising];
|
||||
});
|
||||
OCMStub([_cbPeripheralManagerMock stopAdvertising]).andDo(^(NSInvocation *invocation) {
|
||||
_cbAdvertisementData = nil;
|
||||
});
|
||||
OCMStub([_cbPeripheralManagerMock
|
||||
startAdvertising:[OCMArg checkWithBlock:^BOOL(id obj) {
|
||||
_cbAdvertisementData = obj;
|
||||
[_peripheralManager peripheralManagerDidStartAdvertising:_cbPeripheralManagerMock
|
||||
error:nil];
|
||||
return YES;
|
||||
}]]);
|
||||
_peripheralManager = [[TestGNSPeripheralManager alloc]
|
||||
initWithAdvertisedName:_displayName
|
||||
restoreIdentifier:_restoreIdentifier
|
||||
queue:dispatch_get_main_queue()];
|
||||
_peripheralManager.cbPeripheralManagerMock = _cbPeripheralManagerMock;
|
||||
XCTAssertFalse(_peripheralManager.isStarted);
|
||||
}
|
||||
|
||||
- (void)tearDown {
|
||||
if (_peripheralManager.isStarted) {
|
||||
[self stopPeripheralManager];
|
||||
}
|
||||
if (_peripheralManager.cbPeripheralManager) {
|
||||
NSDictionary *expectedOptions =
|
||||
@{CBPeripheralManagerOptionRestoreIdentifierKey : _restoreIdentifier};
|
||||
XCTAssertEqualObjects(expectedOptions, _peripheralManager.cbOptions);
|
||||
OCMVerifyAll(_peripheralManager.cbPeripheralManagerMock);
|
||||
}
|
||||
for (id mock in _mocksToVerify) {
|
||||
OCMVerifyAll(mock);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Start/Stop
|
||||
|
||||
- (void)startPeripheralManagerWithPeripheralManagerState:(CBPeripheralManagerState)state
|
||||
expectedAdvertisementData:(NSDictionary *)expectedAdvertisement {
|
||||
[_peripheralManager start];
|
||||
[self updatePeripheralManagerWithPeripheralManagerState:state];
|
||||
[self checkAdvertisementData:expectedAdvertisement];
|
||||
}
|
||||
|
||||
- (void)updatePeripheralManagerWithPeripheralManagerState:(CBPeripheralManagerState)state {
|
||||
_cbPeripheralManagerState = state;
|
||||
[_peripheralManager peripheralManagerDidUpdateState:_cbPeripheralManagerMock];
|
||||
XCTAssertTrue(_peripheralManager.isStarted);
|
||||
}
|
||||
|
||||
- (void)checkAdvertisementData:(NSDictionary *)expectedAdvertisement {
|
||||
if (expectedAdvertisement) {
|
||||
XCTAssertEqualObjects(expectedAdvertisement[CBAdvertisementDataLocalNameKey],
|
||||
_cbAdvertisementData[CBAdvertisementDataLocalNameKey]);
|
||||
NSSet *expectedUUIDs =
|
||||
[NSSet setWithArray:expectedAdvertisement[CBAdvertisementDataServiceUUIDsKey]];
|
||||
NSSet *advertisedUUIDs =
|
||||
[NSSet setWithArray:_cbAdvertisementData[CBAdvertisementDataServiceUUIDsKey]];
|
||||
XCTAssertEqualObjects(expectedUUIDs, advertisedUUIDs);
|
||||
} else {
|
||||
XCTAssertNil(_cbAdvertisementData);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updatePeripheralManagerWithPeripheralManagerState:(CBPeripheralManagerState)state
|
||||
expectedAdvertisementData:(NSDictionary *)expectedAdvertisement {
|
||||
_cbPeripheralManagerState = state;
|
||||
[_peripheralManager peripheralManagerDidUpdateState:_cbPeripheralManagerMock];
|
||||
XCTAssertTrue(_peripheralManager.isStarted);
|
||||
if (expectedAdvertisement) {
|
||||
XCTAssertEqualObjects(expectedAdvertisement[CBAdvertisementDataLocalNameKey],
|
||||
_cbAdvertisementData[CBAdvertisementDataLocalNameKey]);
|
||||
NSSet *expectedUUIDs =
|
||||
[NSSet setWithArray:expectedAdvertisement[CBAdvertisementDataServiceUUIDsKey]];
|
||||
NSSet *advertisedUUIDs =
|
||||
[NSSet setWithArray:_cbAdvertisementData[CBAdvertisementDataServiceUUIDsKey]];
|
||||
XCTAssertEqualObjects(expectedUUIDs, advertisedUUIDs);
|
||||
} else {
|
||||
XCTAssertNil(_cbAdvertisementData);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)stopPeripheralManager {
|
||||
OCMExpect([_cbPeripheralManagerMock removeAllServices]);
|
||||
OCMExpect([_cbPeripheralManagerMock setDelegate:nil]);
|
||||
[_peripheralManager stop];
|
||||
XCTAssertNil(_cbAdvertisementData);
|
||||
XCTAssertFalse(_peripheralManager.isStarted);
|
||||
}
|
||||
|
||||
// Starts with bluetooth off.
|
||||
|
||||
- (void)testStartWithNoServiceBluetoothResetting {
|
||||
[_peripheralManager start];
|
||||
OCMExpect([_peripheralManager.cbPeripheralManager removeAllServices]);
|
||||
[self updatePeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStateResetting];
|
||||
[self checkAdvertisementData:nil];
|
||||
}
|
||||
|
||||
- (void)testStartWithNoServiceBluetoothUnknown {
|
||||
[_peripheralManager start];
|
||||
OCMExpect([_peripheralManager.cbPeripheralManager removeAllServices]);
|
||||
[self updatePeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStateUnknown];
|
||||
[self checkAdvertisementData:nil];
|
||||
}
|
||||
|
||||
- (void)testStartWithNoServiceBluetoothUnauthorized {
|
||||
[_peripheralManager start];
|
||||
OCMExpect([_peripheralManager.cbPeripheralManager removeAllServices]);
|
||||
[self updatePeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStateUnauthorized];
|
||||
[self checkAdvertisementData:nil];
|
||||
}
|
||||
|
||||
- (void)testStartWithNoServiceBluetoothUnsupported {
|
||||
OCMStub([_cbPeripheralManagerMock isAdvertising]).andReturn(NO);
|
||||
[self startPeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStateUnsupported
|
||||
expectedAdvertisementData:nil];
|
||||
}
|
||||
|
||||
- (void)testStartWithNoServiceBluetoothOff {
|
||||
NSDictionary *expectedAdvertisingData = @{
|
||||
CBAdvertisementDataServiceUUIDsKey : @[],
|
||||
CBAdvertisementDataLocalNameKey : _displayName,
|
||||
};
|
||||
[self startPeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStatePoweredOff
|
||||
expectedAdvertisementData:expectedAdvertisingData];
|
||||
}
|
||||
|
||||
// Starts with bluetooth on: should advertise local name on BLE.
|
||||
- (void)testStartWithNoServiceBluetoothOn {
|
||||
NSDictionary *expectedAdvertisingData = @{
|
||||
CBAdvertisementDataServiceUUIDsKey : @[],
|
||||
CBAdvertisementDataLocalNameKey : _displayName,
|
||||
};
|
||||
[self startPeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStatePoweredOn
|
||||
expectedAdvertisementData:expectedAdvertisingData];
|
||||
}
|
||||
|
||||
// Starts with BLE unknown, and then turn on bluetooth. Advertisement should be done only after
|
||||
// turning on bluetooth.
|
||||
- (void)testStartWithBleStateUnknownAndTurnOnBluetooth {
|
||||
[_peripheralManager start];
|
||||
OCMExpect([_peripheralManager.cbPeripheralManager removeAllServices]);
|
||||
[self updatePeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStateUnknown];
|
||||
[self checkAdvertisementData:nil];
|
||||
|
||||
[self updatePeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStatePoweredOn
|
||||
expectedAdvertisementData:@{
|
||||
CBAdvertisementDataServiceUUIDsKey : @[],
|
||||
CBAdvertisementDataLocalNameKey : _displayName,
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testStartWithBleStateResettingAndTurnOnBluetooth {
|
||||
[_peripheralManager start];
|
||||
OCMExpect([_peripheralManager.cbPeripheralManager removeAllServices]);
|
||||
[self updatePeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStateUnknown];
|
||||
[self checkAdvertisementData:nil];
|
||||
|
||||
[self updatePeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStatePoweredOn
|
||||
expectedAdvertisementData:@{
|
||||
CBAdvertisementDataServiceUUIDsKey : @[],
|
||||
CBAdvertisementDataLocalNameKey : _displayName,
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Restore
|
||||
|
||||
- (void)testRestore {
|
||||
[_peripheralManager start];
|
||||
GNSPeripheralServiceManager *service1 =
|
||||
[self peripheralServiceManagerWithServiceState:GNSBluetoothServiceStateAdded];
|
||||
OCMStub([service1 isAdvertising]).andReturn(YES);
|
||||
[_peripheralManager addPeripheralServiceManager:service1 bleServiceAddedCompletion:nil];
|
||||
GNSPeripheralServiceManager *service2 =
|
||||
[self peripheralServiceManagerWithServiceState:GNSBluetoothServiceStateAdded];
|
||||
OCMStub([service2 isAdvertising]).andReturn(YES);
|
||||
[_peripheralManager addPeripheralServiceManager:service2 bleServiceAddedCompletion:nil];
|
||||
CBMutableService *restoredCBService1 = OCMStrictClassMock([CBMutableService class]);
|
||||
OCMStub([restoredCBService1 UUID]).andReturn(service1.serviceUUID);
|
||||
CBMutableService *restoredCBService2 = OCMStrictClassMock([CBMutableService class]);
|
||||
OCMStub([restoredCBService2 UUID]).andReturn(service2.serviceUUID);
|
||||
NSDictionary *state = @{
|
||||
CBPeripheralManagerRestoredStateAdvertisementDataKey : @"Name",
|
||||
CBPeripheralManagerRestoredStateServicesKey : @[ restoredCBService1, restoredCBService2 ],
|
||||
};
|
||||
OCMExpect([service1 restoredCBService:restoredCBService1]);
|
||||
OCMExpect([service2 restoredCBService:restoredCBService2]);
|
||||
[_peripheralManager peripheralManager:_cbPeripheralManagerMock willRestoreState:state];
|
||||
[self updatePeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStatePoweredOn];
|
||||
[self checkAdvertisementData:@{
|
||||
CBAdvertisementDataServiceUUIDsKey : @[ service1.serviceUUID, service2.serviceUUID ],
|
||||
CBAdvertisementDataLocalNameKey : _displayName,
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Advertising
|
||||
|
||||
// Generates a peripheral service manager mock ready to be added to |_peripheralManager|.
|
||||
- (GNSPeripheralServiceManager *)peripheralServiceManager {
|
||||
return [self peripheralServiceManagerWithServiceState:GNSBluetoothServiceStateNotAdded];
|
||||
}
|
||||
|
||||
// Generates a peripheral service manager mock |_peripheralManager| with the cbServiceState
|
||||
// set to |initialServiceState|.
|
||||
- (GNSPeripheralServiceManager *)peripheralServiceManagerWithServiceState:
|
||||
(GNSBluetoothServiceState)initialServiceState {
|
||||
CBUUID *uuid = [CBUUID UUIDWithNSUUID:[NSUUID UUID]];
|
||||
CBMutableService *cbService = OCMClassMock([CBMutableService class]);
|
||||
OCMStub([cbService UUID]).andReturn(uuid);
|
||||
[_mocksToVerify addObject:cbService];
|
||||
GNSPeripheralServiceManager *peripheralServiceManager =
|
||||
OCMStrictClassMock([GNSPeripheralServiceManager class]);
|
||||
[_mocksToVerify addObject:peripheralServiceManager];
|
||||
OCMStub([peripheralServiceManager cbService]).andReturn(cbService);
|
||||
OCMStub([peripheralServiceManager serviceUUID]).andReturn(uuid);
|
||||
OCMExpect([peripheralServiceManager addedToPeripheralManager:_peripheralManager
|
||||
bleServiceAddedCompletion:[OCMArg any]]);
|
||||
|
||||
__block GNSBluetoothServiceState cbServiceState = initialServiceState;
|
||||
OCMStub([peripheralServiceManager cbServiceState]).andDo(^(NSInvocation *invocation) {
|
||||
GNSBluetoothServiceState stateToReturn = cbServiceState;
|
||||
return [invocation setReturnValue:&stateToReturn];
|
||||
});
|
||||
OCMStub([peripheralServiceManager willAddCBService]).andDo(^(NSInvocation *invocation) {
|
||||
cbServiceState = GNSBluetoothServiceStateAddInProgress;
|
||||
});
|
||||
|
||||
OCMStub([peripheralServiceManager didAddCBServiceWithError:[OCMArg any]])
|
||||
.andDo(^(NSInvocation *invocation) {
|
||||
__unsafe_unretained NSError *error = nil;
|
||||
[invocation getArgument:&error atIndex:2];
|
||||
if (!error) {
|
||||
cbServiceState = GNSBluetoothServiceStateAdded;
|
||||
} else {
|
||||
cbServiceState = GNSBluetoothServiceStateNotAdded;
|
||||
}
|
||||
});
|
||||
OCMStub([peripheralServiceManager didRemoveCBService]).andDo(^(NSInvocation *invocation) {
|
||||
cbServiceState = GNSBluetoothServiceStateNotAdded;
|
||||
});
|
||||
return peripheralServiceManager;
|
||||
}
|
||||
|
||||
// Adds a peripheral service manager mock into |_peripheralManager|.
|
||||
- (void)addPeripheralManager:(id)peripheralServiceManager {
|
||||
CBMutableService *service = [peripheralServiceManager cbService];
|
||||
OCMExpect([_cbPeripheralManagerMock addService:service]);
|
||||
[_peripheralManager addPeripheralServiceManager:peripheralServiceManager
|
||||
bleServiceAddedCompletion:nil];
|
||||
}
|
||||
|
||||
// Adds one peripheral service manager.
|
||||
- (void)testAddOneServiceStartsAdvertisment {
|
||||
GNSPeripheralServiceManager *peripheralServiceManager = [self peripheralServiceManager];
|
||||
OCMStub([peripheralServiceManager isAdvertising]).andReturn(YES);
|
||||
// Starting before the services are added should not start adverting.
|
||||
[self addPeripheralManager:peripheralServiceManager];
|
||||
[_peripheralManager start];
|
||||
XCTAssertEqual(GNSBluetoothServiceStateNotAdded, [peripheralServiceManager cbServiceState]);
|
||||
[self updatePeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStatePoweredOn];
|
||||
XCTAssertEqual(GNSBluetoothServiceStateAddInProgress, [peripheralServiceManager cbServiceState]);
|
||||
[self checkAdvertisementData:nil];
|
||||
[_peripheralManager peripheralManager:_cbPeripheralManagerMock
|
||||
didAddService:[peripheralServiceManager cbService]
|
||||
error:nil];
|
||||
XCTAssertEqual(GNSBluetoothServiceStateAdded, [peripheralServiceManager cbServiceState]);
|
||||
|
||||
// Adding the service should start the advertisment.
|
||||
[self checkAdvertisementData:@{
|
||||
CBAdvertisementDataServiceUUIDsKey : @[ [peripheralServiceManager serviceUUID] ],
|
||||
CBAdvertisementDataLocalNameKey : _displayName,
|
||||
}];
|
||||
}
|
||||
|
||||
// Adds one peripheral service manager fails with error.
|
||||
- (void)testAddOneServiceError {
|
||||
GNSPeripheralServiceManager *peripheralServiceManager = [self peripheralServiceManager];
|
||||
OCMStub([peripheralServiceManager isAdvertising]).andReturn(YES);
|
||||
[self addPeripheralManager:peripheralServiceManager];
|
||||
[_peripheralManager start];
|
||||
[self updatePeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStatePoweredOn];
|
||||
NSError *expectedError = [[NSError alloc] initWithDomain:@"Test" code:-42 userInfo:nil];
|
||||
[_peripheralManager peripheralManager:_cbPeripheralManagerMock
|
||||
didAddService:[peripheralServiceManager cbService]
|
||||
error:expectedError];
|
||||
[self checkAdvertisementData:nil];
|
||||
XCTAssertEqual(GNSBluetoothServiceStateNotAdded, [peripheralServiceManager cbServiceState]);
|
||||
}
|
||||
|
||||
// Adds two peripheral service managers, one that advertise itself, and one that doesn't. Only
|
||||
// one service should be advertised.
|
||||
- (void)testAddTwoServices {
|
||||
GNSPeripheralServiceManager *peripheralServiceManager1 = [self peripheralServiceManager];
|
||||
OCMStub([peripheralServiceManager1 isAdvertising]).andReturn(NO);
|
||||
[self addPeripheralManager:peripheralServiceManager1];
|
||||
GNSPeripheralServiceManager *peripheralServiceManager2 = [self peripheralServiceManager];
|
||||
OCMStub([peripheralServiceManager2 isAdvertising]).andReturn(YES);
|
||||
[self addPeripheralManager:peripheralServiceManager2];
|
||||
[_peripheralManager start];
|
||||
[self updatePeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStatePoweredOn];
|
||||
[_peripheralManager peripheralManager:_cbPeripheralManagerMock
|
||||
didAddService:[peripheralServiceManager1 cbService]
|
||||
error:nil];
|
||||
[_peripheralManager peripheralManager:_cbPeripheralManagerMock
|
||||
didAddService:[peripheralServiceManager2 cbService]
|
||||
error:nil];
|
||||
NSDictionary *expectedAdvertisement = @{
|
||||
CBAdvertisementDataServiceUUIDsKey : @[ [peripheralServiceManager2 serviceUUID] ],
|
||||
CBAdvertisementDataLocalNameKey : _displayName,
|
||||
};
|
||||
[self checkAdvertisementData:expectedAdvertisement];
|
||||
}
|
||||
|
||||
// Adds one peripheral service manager while not advertising. Changes the advertising value for this
|
||||
// peripheral service manager.
|
||||
- (void)testAddServiceAndChangeAdvertisingValue {
|
||||
GNSPeripheralServiceManager *peripheralServiceManager = [self peripheralServiceManager];
|
||||
OCMStubRecorder *isAdvertisingStub = OCMStub([peripheralServiceManager isAdvertising]);
|
||||
isAdvertisingStub.andReturn(NO);
|
||||
[self addPeripheralManager:peripheralServiceManager];
|
||||
[_peripheralManager start];
|
||||
[self updatePeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStatePoweredOn];
|
||||
[self checkAdvertisementData:nil];
|
||||
XCTAssertEqual(GNSBluetoothServiceStateAddInProgress, [peripheralServiceManager cbServiceState]);
|
||||
[_peripheralManager peripheralManager:_cbPeripheralManagerMock
|
||||
didAddService:[peripheralServiceManager cbService]
|
||||
error:nil];
|
||||
[self checkAdvertisementData:@{
|
||||
CBAdvertisementDataServiceUUIDsKey : @[],
|
||||
CBAdvertisementDataLocalNameKey : _displayName,
|
||||
}];
|
||||
isAdvertisingStub.andReturn(YES);
|
||||
[_peripheralManager updateAdvertisedServices];
|
||||
[self checkAdvertisementData:@{
|
||||
CBAdvertisementDataServiceUUIDsKey : @[ [peripheralServiceManager serviceUUID] ],
|
||||
CBAdvertisementDataLocalNameKey : _displayName,
|
||||
}];
|
||||
return;
|
||||
}
|
||||
|
||||
- (void)startWithPeripheralServiceManagers:(NSArray *)managers {
|
||||
NSMutableArray *advertisedServiceUUIDs = [NSMutableArray array];
|
||||
for (GNSPeripheralServiceManager *peripheralServiceManager in managers) {
|
||||
[self addPeripheralManager:peripheralServiceManager];
|
||||
if (peripheralServiceManager.isAdvertising) {
|
||||
[advertisedServiceUUIDs addObject:peripheralServiceManager.serviceUUID];
|
||||
}
|
||||
}
|
||||
[_peripheralManager start];
|
||||
[self updatePeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStatePoweredOn];
|
||||
for (GNSPeripheralServiceManager *peripheralServiceManager in managers) {
|
||||
[_peripheralManager peripheralManager:_cbPeripheralManagerMock
|
||||
didAddService:[peripheralServiceManager cbService]
|
||||
error:nil];
|
||||
}
|
||||
[self checkAdvertisementData:@{
|
||||
CBAdvertisementDataServiceUUIDsKey : advertisedServiceUUIDs,
|
||||
CBAdvertisementDataLocalNameKey : _displayName,
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Subscribe/unsubscribe characteristics
|
||||
|
||||
// Creates a characteristic mock, and adds it into |_mocksToVerify|. If no service is provided,
|
||||
// a service mock is created (added into |_mocksToVerify| too). The service uuid is attach
|
||||
// to the service. If no service uuid is provided, an uuid is created.
|
||||
- (CBMutableCharacteristic *)prepareCharacteristicForCBService:(CBService *)cbService
|
||||
withServiceUUID:(CBUUID *)serviceUUID {
|
||||
if (!serviceUUID) {
|
||||
serviceUUID = [CBUUID UUIDWithNSUUID:[NSUUID UUID]];
|
||||
}
|
||||
if (!cbService) {
|
||||
cbService = OCMClassMock([CBService class]);
|
||||
OCMStub([cbService UUID]).andReturn(serviceUUID);
|
||||
[_mocksToVerify addObject:cbService];
|
||||
}
|
||||
CBMutableCharacteristic *characteristicMock = OCMClassMock([CBMutableCharacteristic class]);
|
||||
OCMStub([characteristicMock service]).andReturn(cbService);
|
||||
[_mocksToVerify addObject:characteristicMock];
|
||||
return characteristicMock;
|
||||
}
|
||||
|
||||
// Creates a characteristic mock based on the service from the peripheral service manager.
|
||||
// The mock is added into _mocksToVerify.
|
||||
- (CBMutableCharacteristic *)prepareCharacteristicForPeripheralServiceManager:
|
||||
(GNSPeripheralServiceManager *)peripheralServiceManager {
|
||||
CBService *cbService = peripheralServiceManager.cbService;
|
||||
CBUUID *serviceUUID = cbService.UUID;
|
||||
return [self prepareCharacteristicForCBService:cbService withServiceUUID:serviceUUID];
|
||||
}
|
||||
|
||||
// Adds a peripheral service manager, and a central subscribes to its service.
|
||||
- (void)testCentralDidSubscribe {
|
||||
id peripheralServiceManager = [self peripheralServiceManager];
|
||||
OCMStub([peripheralServiceManager isAdvertising]).andReturn(YES);
|
||||
[self startWithPeripheralServiceManagers:@[ peripheralServiceManager ]];
|
||||
id centralMock = OCMClassMock([CBCentral class]);
|
||||
CBMutableCharacteristic *characteristicMock =
|
||||
[self prepareCharacteristicForPeripheralServiceManager:peripheralServiceManager];
|
||||
OCMExpect([peripheralServiceManager central:centralMock
|
||||
didSubscribeToCharacteristic:characteristicMock]);
|
||||
[_peripheralManager peripheralManager:_cbPeripheralManagerMock
|
||||
central:centralMock
|
||||
didSubscribeToCharacteristic:characteristicMock];
|
||||
OCMVerifyAll(centralMock);
|
||||
}
|
||||
|
||||
// Adds a peripheral service manager, and a central unsubscribes to its service.
|
||||
- (void)testCentralDidUnsubscribe {
|
||||
id peripheralServiceManager = [self peripheralServiceManager];
|
||||
OCMStub([peripheralServiceManager isAdvertising]).andReturn(YES);
|
||||
[self startWithPeripheralServiceManagers:@[ peripheralServiceManager ]];
|
||||
id centralMock = OCMClassMock([CBCentral class]);
|
||||
CBMutableCharacteristic *characteristicMock =
|
||||
[self prepareCharacteristicForPeripheralServiceManager:peripheralServiceManager];
|
||||
OCMExpect([peripheralServiceManager central:centralMock
|
||||
didUnsubscribeFromCharacteristic:characteristicMock]);
|
||||
|
||||
// As a workaourd for b/31752176 We are restarting the peripheral manager after the central
|
||||
// unsubscribe.
|
||||
OCMExpect([_cbPeripheralManagerMock removeAllServices]);
|
||||
OCMExpect([_cbPeripheralManagerMock setDelegate:nil]);
|
||||
|
||||
[_peripheralManager peripheralManager:_cbPeripheralManagerMock
|
||||
central:centralMock
|
||||
didUnsubscribeFromCharacteristic:characteristicMock];
|
||||
OCMVerifyAll(centralMock);
|
||||
}
|
||||
|
||||
#pragma mark - Characteristic requests
|
||||
|
||||
// Creates an ATT request mock based on the characteristic. The mock is added into |_mocksToVerify|
|
||||
- (CBATTRequest *)prepareRequestForCharacteristic:(CBMutableCharacteristic *)characteristic {
|
||||
id requestMock = OCMClassMock([CBATTRequest class]);
|
||||
OCMStub([requestMock characteristic]).andReturn(characteristic);
|
||||
[_mocksToVerify addObject:requestMock];
|
||||
return requestMock;
|
||||
}
|
||||
|
||||
// Adds a peripheral service manager, and receives a read request accepted by the manager. The
|
||||
// read request should be processed.
|
||||
- (void)testDidReceiveReadRequest {
|
||||
id peripheralServiceManager = [self peripheralServiceManager];
|
||||
OCMStub([peripheralServiceManager isAdvertising]).andReturn(YES);
|
||||
[self startWithPeripheralServiceManagers:@[ peripheralServiceManager ]];
|
||||
CBMutableCharacteristic *characteristicMock =
|
||||
[self prepareCharacteristicForPeripheralServiceManager:peripheralServiceManager];
|
||||
CBATTRequest *readRequestMock = [self prepareRequestForCharacteristic:characteristicMock];
|
||||
OCMStub([peripheralServiceManager canProcessReadRequest:readRequestMock])
|
||||
.andReturn(CBATTErrorSuccess);
|
||||
OCMExpect([peripheralServiceManager processReadRequest:readRequestMock]);
|
||||
OCMExpect(
|
||||
[_cbPeripheralManagerMock respondToRequest:readRequestMock withResult:CBATTErrorSuccess]);
|
||||
[_peripheralManager peripheralManager:_cbPeripheralManagerMock
|
||||
didReceiveReadRequest:readRequestMock];
|
||||
}
|
||||
|
||||
// Adds a peripheral service manager, and receives a read request refused by the manager. The
|
||||
// read request should not be processed.
|
||||
- (void)testDidReceiveBadReadRequest {
|
||||
id peripheralServiceManager = [self peripheralServiceManager];
|
||||
OCMStub([peripheralServiceManager isAdvertising]).andReturn(YES);
|
||||
[self startWithPeripheralServiceManagers:@[ peripheralServiceManager ]];
|
||||
CBMutableCharacteristic *characteristicMock =
|
||||
[self prepareCharacteristicForPeripheralServiceManager:peripheralServiceManager];
|
||||
CBATTRequest *readRequestMock = [self prepareRequestForCharacteristic:characteristicMock];
|
||||
OCMStub([peripheralServiceManager canProcessReadRequest:readRequestMock])
|
||||
.andReturn(CBATTErrorReadNotPermitted);
|
||||
OCMExpect([_cbPeripheralManagerMock respondToRequest:readRequestMock
|
||||
withResult:CBATTErrorReadNotPermitted]);
|
||||
[_peripheralManager peripheralManager:_cbPeripheralManagerMock
|
||||
didReceiveReadRequest:readRequestMock];
|
||||
}
|
||||
|
||||
// Adds a peripheral service manager, and receives a read request to another service. The read
|
||||
// request should not be processed.
|
||||
- (void)testDidReceiveReadRequestToWrongService {
|
||||
id peripheralServiceManager = [self peripheralServiceManager];
|
||||
OCMStub([peripheralServiceManager isAdvertising]).andReturn(YES);
|
||||
[self startWithPeripheralServiceManagers:@[ peripheralServiceManager ]];
|
||||
CBMutableCharacteristic *characteristicMock =
|
||||
[self prepareCharacteristicForCBService:nil withServiceUUID:nil];
|
||||
CBATTRequest *readRequestMock = [self prepareRequestForCharacteristic:characteristicMock];
|
||||
OCMExpect([_cbPeripheralManagerMock respondToRequest:readRequestMock
|
||||
withResult:CBATTErrorAttributeNotFound]);
|
||||
[_peripheralManager peripheralManager:_cbPeripheralManagerMock
|
||||
didReceiveReadRequest:readRequestMock];
|
||||
}
|
||||
|
||||
// Adds a peripheral service manager, and receives a write request accepted by the manager. The
|
||||
// write request should be processed.
|
||||
- (void)testDidReceiveOneWriteRequest {
|
||||
id peripheralServiceManager = [self peripheralServiceManager];
|
||||
OCMStub([peripheralServiceManager isAdvertising]).andReturn(YES);
|
||||
[self startWithPeripheralServiceManagers:@[ peripheralServiceManager ]];
|
||||
CBMutableCharacteristic *characteristicMock =
|
||||
[self prepareCharacteristicForPeripheralServiceManager:peripheralServiceManager];
|
||||
CBATTRequest *writeRequestMock = [self prepareRequestForCharacteristic:characteristicMock];
|
||||
OCMStub([peripheralServiceManager canProcessWriteRequest:writeRequestMock])
|
||||
.andReturn(CBATTErrorSuccess);
|
||||
OCMExpect([peripheralServiceManager processWriteRequest:writeRequestMock]);
|
||||
OCMExpect(
|
||||
[_cbPeripheralManagerMock respondToRequest:writeRequestMock withResult:CBATTErrorSuccess]);
|
||||
[_peripheralManager peripheralManager:_cbPeripheralManagerMock
|
||||
didReceiveWriteRequests:@[ writeRequestMock ]];
|
||||
}
|
||||
|
||||
// Adds a peripheral service manager, and receives a write request refused by the manager. The write
|
||||
// request should not be processed.
|
||||
- (void)testDidReceiveOneBadWriteRequest {
|
||||
id peripheralServiceManager = [self peripheralServiceManager];
|
||||
OCMStub([peripheralServiceManager isAdvertising]).andReturn(YES);
|
||||
[self startWithPeripheralServiceManagers:@[ peripheralServiceManager ]];
|
||||
CBMutableCharacteristic *characteristicMock =
|
||||
[self prepareCharacteristicForPeripheralServiceManager:peripheralServiceManager];
|
||||
CBATTRequest *writeRequestMock = [self prepareRequestForCharacteristic:characteristicMock];
|
||||
OCMStub([peripheralServiceManager canProcessWriteRequest:writeRequestMock])
|
||||
.andReturn(CBATTErrorWriteNotPermitted);
|
||||
OCMExpect([_cbPeripheralManagerMock respondToRequest:writeRequestMock
|
||||
withResult:CBATTErrorWriteNotPermitted]);
|
||||
[_peripheralManager peripheralManager:_cbPeripheralManagerMock
|
||||
didReceiveWriteRequests:@[ writeRequestMock ]];
|
||||
}
|
||||
|
||||
// Adds a peripheral service manager, and receives a write request to another service. The write
|
||||
// request should not be processed.
|
||||
- (void)testDidReceiveOneWriteRequestToWrongService {
|
||||
id peripheralServiceManager = [self peripheralServiceManager];
|
||||
OCMStub([peripheralServiceManager isAdvertising]).andReturn(YES);
|
||||
[self startWithPeripheralServiceManagers:@[ peripheralServiceManager ]];
|
||||
CBMutableCharacteristic *characteristicMock =
|
||||
[self prepareCharacteristicForCBService:nil withServiceUUID:nil];
|
||||
CBATTRequest *writeRequestMock = [self prepareRequestForCharacteristic:characteristicMock];
|
||||
OCMStub([_cbPeripheralManagerMock respondToRequest:writeRequestMock
|
||||
withResult:CBATTErrorAttributeNotFound]);
|
||||
[_peripheralManager peripheralManager:_cbPeripheralManagerMock
|
||||
didReceiveWriteRequests:@[ writeRequestMock ]];
|
||||
}
|
||||
|
||||
// Adds a peripheral service manager, and receives two write requests, only the second one is
|
||||
// refused by the manager. None of those write requests should be processed. The error should be
|
||||
// sent to the first write request.
|
||||
- (void)testDidReceiveWriteRequestOneGoodAndOneBad {
|
||||
id peripheralServiceManager1 = [self peripheralServiceManager];
|
||||
OCMStub([peripheralServiceManager1 isAdvertising]).andReturn(YES);
|
||||
id peripheralServiceManager2 = [self peripheralServiceManager];
|
||||
OCMStub([peripheralServiceManager2 isAdvertising]).andReturn(YES);
|
||||
[self
|
||||
startWithPeripheralServiceManagers:@[ peripheralServiceManager1, peripheralServiceManager2 ]];
|
||||
// Good request
|
||||
CBMutableCharacteristic *characteristic1Mock =
|
||||
[self prepareCharacteristicForPeripheralServiceManager:peripheralServiceManager1];
|
||||
CBATTRequest *writeRequest1Mock = [self prepareRequestForCharacteristic:characteristic1Mock];
|
||||
OCMStub([peripheralServiceManager1 canProcessWriteRequest:writeRequest1Mock])
|
||||
.andReturn(CBATTErrorSuccess);
|
||||
// Bad request
|
||||
CBMutableCharacteristic *characteristic2Mock =
|
||||
[self prepareCharacteristicForPeripheralServiceManager:peripheralServiceManager2];
|
||||
CBATTRequest *writeRequest2Mock = [self prepareRequestForCharacteristic:characteristic2Mock];
|
||||
OCMStub([peripheralServiceManager2 canProcessWriteRequest:writeRequest2Mock])
|
||||
.andReturn(CBATTErrorWriteNotPermitted);
|
||||
OCMExpect([_cbPeripheralManagerMock respondToRequest:writeRequest1Mock
|
||||
withResult:CBATTErrorWriteNotPermitted]);
|
||||
[_peripheralManager peripheralManager:_cbPeripheralManagerMock
|
||||
didReceiveWriteRequests:@[ writeRequest1Mock, writeRequest2Mock ]];
|
||||
}
|
||||
|
||||
// Adds a peripheral service manager, and receives two write requests, only the first one is
|
||||
// refused by the manager. None of those write requests should be processed. The error should be
|
||||
// sent to the first write request.
|
||||
- (void)testDidReceiveWriteRequestOneBadAndOneGood {
|
||||
id peripheralServiceManager1 = [self peripheralServiceManager];
|
||||
OCMStub([peripheralServiceManager1 isAdvertising]).andReturn(YES);
|
||||
id peripheralServiceManager2 = [self peripheralServiceManager];
|
||||
OCMStub([peripheralServiceManager2 isAdvertising]).andReturn(YES);
|
||||
[self
|
||||
startWithPeripheralServiceManagers:@[ peripheralServiceManager1, peripheralServiceManager2 ]];
|
||||
// Good request
|
||||
CBMutableCharacteristic *characteristic1Mock =
|
||||
[self prepareCharacteristicForPeripheralServiceManager:peripheralServiceManager1];
|
||||
CBATTRequest *writeRequest1Mock = [self prepareRequestForCharacteristic:characteristic1Mock];
|
||||
OCMStub([peripheralServiceManager1 canProcessWriteRequest:writeRequest1Mock])
|
||||
.andReturn(CBATTErrorWriteNotPermitted);
|
||||
// Bad request
|
||||
CBMutableCharacteristic *characteristic2Mock =
|
||||
[self prepareCharacteristicForPeripheralServiceManager:peripheralServiceManager2];
|
||||
CBATTRequest *writeRequest2Mock = [self prepareRequestForCharacteristic:characteristic2Mock];
|
||||
OCMStub([peripheralServiceManager2 canProcessWriteRequest:writeRequest2Mock])
|
||||
.andReturn(CBATTErrorSuccess);
|
||||
OCMExpect([_cbPeripheralManagerMock respondToRequest:writeRequest1Mock
|
||||
withResult:CBATTErrorWriteNotPermitted]);
|
||||
[_peripheralManager peripheralManager:_cbPeripheralManagerMock
|
||||
didReceiveWriteRequests:@[ writeRequest1Mock, writeRequest2Mock ]];
|
||||
}
|
||||
|
||||
// Adds a peripheral service manager, and receives two good write requests. Both write request
|
||||
// should be processed. The success should be sent to the first write request.
|
||||
- (void)testDidReceiveWriteRequestTwoGood {
|
||||
id peripheralServiceManager1 = [self peripheralServiceManager];
|
||||
OCMStub([peripheralServiceManager1 isAdvertising]).andReturn(YES);
|
||||
id peripheralServiceManager2 = [self peripheralServiceManager];
|
||||
OCMStub([peripheralServiceManager2 isAdvertising]).andReturn(YES);
|
||||
[self
|
||||
startWithPeripheralServiceManagers:@[ peripheralServiceManager1, peripheralServiceManager2 ]];
|
||||
// Good request
|
||||
CBMutableCharacteristic *characteristic1Mock =
|
||||
[self prepareCharacteristicForPeripheralServiceManager:peripheralServiceManager1];
|
||||
CBATTRequest *writeRequest1Mock = [self prepareRequestForCharacteristic:characteristic1Mock];
|
||||
OCMStub([peripheralServiceManager1 canProcessWriteRequest:writeRequest1Mock])
|
||||
.andReturn(CBATTErrorSuccess);
|
||||
OCMExpect([peripheralServiceManager1 processWriteRequest:writeRequest1Mock]);
|
||||
// Good request
|
||||
CBMutableCharacteristic *characteristic2Mock =
|
||||
[self prepareCharacteristicForPeripheralServiceManager:peripheralServiceManager2];
|
||||
CBATTRequest *writeRequest2Mock = [self prepareRequestForCharacteristic:characteristic2Mock];
|
||||
OCMStub([peripheralServiceManager2 canProcessWriteRequest:writeRequest2Mock])
|
||||
.andReturn(CBATTErrorSuccess);
|
||||
OCMExpect([peripheralServiceManager2 processWriteRequest:writeRequest2Mock]);
|
||||
OCMExpect(
|
||||
[_cbPeripheralManagerMock respondToRequest:writeRequest1Mock withResult:CBATTErrorSuccess]);
|
||||
[_peripheralManager peripheralManager:_cbPeripheralManagerMock
|
||||
didReceiveWriteRequests:@[ writeRequest1Mock, writeRequest2Mock ]];
|
||||
}
|
||||
|
||||
- (GNSSocket *)createSocketMock {
|
||||
GNSSocket *socketMock = OCMStrictClassMock([GNSSocket class]);
|
||||
NSUUID *socketIdentifier = [NSUUID UUID];
|
||||
OCMStub([socketMock socketIdentifier]).andReturn(socketIdentifier);
|
||||
[_mocksToVerify addObject:socketMock];
|
||||
return socketMock;
|
||||
}
|
||||
|
||||
// Adds one update value handler. Should be called right now.
|
||||
- (void)testIsReadyToUpdateSubscribers {
|
||||
__block BOOL updateValueHandlerCalled = NO;
|
||||
GNSSocket *socketMock = [self createSocketMock];
|
||||
// Should be called right now.
|
||||
[_peripheralManager updateOutgoingCharOnSocket:socketMock
|
||||
withHandler:^() {
|
||||
updateValueHandlerCalled = YES;
|
||||
return GNSOutgoingCharUpdateNoReschedule;
|
||||
}];
|
||||
XCTAssertTrue(updateValueHandlerCalled);
|
||||
}
|
||||
|
||||
// Adds one update value handler. The first one fails. Adds a second update value. It should not
|
||||
// be called. The first one is set to not fail anymore. When the CB peripheral manager is ready
|
||||
// to update subscribers, both handlers should be called.
|
||||
- (void)testIsReadyToUpdateSubscriberBlocked {
|
||||
__block BOOL updateValueHandler1Called = NO;
|
||||
__block GNSOutgoingCharUpdate updateValueHandler1ReturnedValue =
|
||||
GNSOutgoingCharUpdateScheduleLater;
|
||||
GNSSocket *socketMock = [self createSocketMock];
|
||||
// Should be called right now.
|
||||
[_peripheralManager updateOutgoingCharOnSocket:socketMock
|
||||
withHandler:^() {
|
||||
updateValueHandler1Called = YES;
|
||||
return updateValueHandler1ReturnedValue;
|
||||
}];
|
||||
XCTAssertTrue(updateValueHandler1Called);
|
||||
__block BOOL updateValueHandler2Called = NO;
|
||||
__block GNSOutgoingCharUpdate updateValueHandler2ReturnedValue =
|
||||
GNSOutgoingCharUpdateNoReschedule;
|
||||
// Should no be called since the previous update value handler failed.
|
||||
[_peripheralManager updateOutgoingCharOnSocket:socketMock
|
||||
withHandler:^() {
|
||||
updateValueHandler2Called = YES;
|
||||
return updateValueHandler2ReturnedValue;
|
||||
}];
|
||||
XCTAssertFalse(updateValueHandler2Called);
|
||||
updateValueHandler1ReturnedValue = GNSOutgoingCharUpdateNoReschedule;
|
||||
updateValueHandler1Called = NO;
|
||||
// Should again again the first handler, and the second right after.
|
||||
[_peripheralManager peripheralManagerIsReadyToUpdateSubscribers:_cbPeripheralManagerMock];
|
||||
XCTAssertTrue(updateValueHandler1Called);
|
||||
XCTAssertTrue(updateValueHandler2Called);
|
||||
}
|
||||
|
||||
// Adds 2 handlers on different sockets. One fails, the second one should process right away.
|
||||
- (void)testTwoUpdateBlocksOnDifferentSocket {
|
||||
__block BOOL updateValueHandler1Called = NO;
|
||||
__block GNSOutgoingCharUpdate updateValueHandler1ReturnedValue =
|
||||
GNSOutgoingCharUpdateScheduleLater;
|
||||
GNSSocket *socketMock1 = [self createSocketMock];
|
||||
// Should be called right now.
|
||||
[_peripheralManager updateOutgoingCharOnSocket:socketMock1
|
||||
withHandler:^() {
|
||||
updateValueHandler1Called = YES;
|
||||
return updateValueHandler1ReturnedValue;
|
||||
}];
|
||||
XCTAssertTrue(updateValueHandler1Called);
|
||||
__block BOOL updateValueHandler2Called = NO;
|
||||
__block GNSOutgoingCharUpdate updateValueHandler2ReturnedValue =
|
||||
GNSOutgoingCharUpdateNoReschedule;
|
||||
GNSSocket *socketMock2 = [self createSocketMock];
|
||||
// Should no be called since the previous update value handler failed.
|
||||
[_peripheralManager updateOutgoingCharOnSocket:socketMock2
|
||||
withHandler:^() {
|
||||
updateValueHandler2Called = YES;
|
||||
return updateValueHandler2ReturnedValue;
|
||||
}];
|
||||
XCTAssertTrue(updateValueHandler2Called);
|
||||
updateValueHandler2Called = NO;
|
||||
updateValueHandler1ReturnedValue = GNSOutgoingCharUpdateNoReschedule;
|
||||
updateValueHandler1Called = NO;
|
||||
// Should again again the first handler, and the second one should not be called.
|
||||
[_peripheralManager peripheralManagerIsReadyToUpdateSubscribers:_cbPeripheralManagerMock];
|
||||
XCTAssertTrue(updateValueHandler1Called);
|
||||
XCTAssertFalse(updateValueHandler2Called);
|
||||
}
|
||||
|
||||
- (void)testUpdateBlockCleanupAfterDisconnect {
|
||||
__block BOOL updateValueHandlerCalled = NO;
|
||||
GNSSocket *socketMock = [self createSocketMock];
|
||||
__block GNSOutgoingCharUpdate outgoingCharUpdateValue = GNSOutgoingCharUpdateScheduleLater;
|
||||
// Should be called right now.
|
||||
[_peripheralManager updateOutgoingCharOnSocket:socketMock
|
||||
withHandler:^() {
|
||||
updateValueHandlerCalled = YES;
|
||||
return outgoingCharUpdateValue;
|
||||
}];
|
||||
XCTAssertTrue(updateValueHandlerCalled);
|
||||
updateValueHandlerCalled = NO;
|
||||
outgoingCharUpdateValue = GNSOutgoingCharUpdateNoReschedule;
|
||||
OCMStub([socketMock isConnected]).andReturn(NO);
|
||||
[_peripheralManager socketDidDisconnect:socketMock];
|
||||
// Should again again the first handler, and the second right after.
|
||||
[_peripheralManager peripheralManagerIsReadyToUpdateSubscribers:_cbPeripheralManagerMock];
|
||||
XCTAssertTrue(updateValueHandlerCalled);
|
||||
}
|
||||
|
||||
- (void)testUpdateValueForCentral {
|
||||
NSData *data = [NSData data];
|
||||
id characteristicMock = OCMStrictClassMock([CBMutableCharacteristic class]);
|
||||
id centralMock = OCMStrictClassMock([CBCentral class]);
|
||||
OCMStub([_cbPeripheralManagerMock updateValue:data
|
||||
forCharacteristic:characteristicMock
|
||||
onSubscribedCentrals:@[ centralMock ]]);
|
||||
GNSPeripheralServiceManager *peripheralServiceManager =
|
||||
OCMStrictClassMock([GNSPeripheralServiceManager class]);
|
||||
OCMStub([peripheralServiceManager weaveOutgoingCharacteristic]).andReturn(characteristicMock);
|
||||
GNSSocket *socketMock = OCMStrictClassMock([GNSSocket class]);
|
||||
OCMStub([socketMock owner]).andReturn(peripheralServiceManager);
|
||||
OCMStub([socketMock peerAsCentral]).andReturn(centralMock);
|
||||
[_peripheralManager updateOutgoingCharacteristic:data onSocket:socketMock];
|
||||
OCMVerifyAll(characteristicMock);
|
||||
OCMVerifyAll(centralMock);
|
||||
}
|
||||
|
||||
- (void)testRecoverFromBTCrashLoop {
|
||||
GNSPeripheralServiceManager *peripheralServiceManager = [self peripheralServiceManager];
|
||||
OCMStub([peripheralServiceManager isAdvertising]).andReturn(YES);
|
||||
[self startWithPeripheralServiceManagers:@[ peripheralServiceManager ]];
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
OCMExpect([_cbPeripheralManagerMock removeAllServices]);
|
||||
[self updatePeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStateResetting];
|
||||
CBMutableService *service = [peripheralServiceManager cbService];
|
||||
OCMExpect([_cbPeripheralManagerMock addService:service]);
|
||||
[self updatePeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStatePoweredOff];
|
||||
XCTAssertEqual(GNSBluetoothServiceStateAddInProgress,
|
||||
[peripheralServiceManager cbServiceState]);
|
||||
}
|
||||
|
||||
// After 5 consecutive resetting state, the services are no longer added by the peripheral
|
||||
// manager.
|
||||
OCMExpect([_cbPeripheralManagerMock removeAllServices]);
|
||||
[self updatePeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStateResetting];
|
||||
[self updatePeripheralManagerWithPeripheralManagerState:CBPeripheralManagerStatePoweredOff];
|
||||
XCTAssertEqual(GNSBluetoothServiceStateNotAdded, [peripheralServiceManager cbServiceState]);
|
||||
}
|
||||
|
||||
@end
|
||||
+671
@@ -0,0 +1,671 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/ble/Sockets/Source/Peripheral/GNSPeripheralManager+Private.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/ble/Sockets/Source/Peripheral/GNSPeripheralServiceManager+Private.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/ble/Sockets/Source/Shared/GNSSocket+Private.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/ble/Sockets/Source/Shared/GNSUtils.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/ble/Sockets/Source/Shared/GNSWeavePacket.h"
|
||||
#import "third_party/objective_c/ocmock/v3/Source/OCMock/OCMock.h"
|
||||
|
||||
@interface GNSPeripheralServiceManagerTest : XCTestCase {
|
||||
GNSPeripheralServiceManager *_peripheralServiceManager;
|
||||
CBUUID *_serviceUUID;
|
||||
BOOL _shouldAcceptSocket;
|
||||
GNSSocket *_receivedSocket;
|
||||
id _peripheralManagerMock;
|
||||
id _cbPeripheralManagerMock;
|
||||
id _socketDelegateMock;
|
||||
NSMutableArray *_mocksToVerify;
|
||||
NSUInteger _centralMaximumUpdateValueLength;
|
||||
NSUInteger _packetSize;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation GNSPeripheralServiceManagerTest
|
||||
|
||||
- (void)setUp {
|
||||
_mocksToVerify = [NSMutableArray array];
|
||||
_serviceUUID = [CBUUID UUIDWithString:@"3C672799-2B3F-4D93-9E57-29D5C5B01092"];;
|
||||
_peripheralManagerMock = OCMStrictClassMock([GNSPeripheralManager class]);
|
||||
_cbPeripheralManagerMock = OCMStrictClassMock([CBPeripheralManager class]);
|
||||
_socketDelegateMock = OCMStrictProtocolMock(@protocol(GNSSocketDelegate));
|
||||
_centralMaximumUpdateValueLength = 100;
|
||||
_packetSize = 100;
|
||||
OCMStub([_peripheralManagerMock cbPeripheralManager]).andReturn(_cbPeripheralManagerMock);
|
||||
_peripheralServiceManager = [[GNSPeripheralServiceManager alloc]
|
||||
initWithBleServiceUUID:_serviceUUID
|
||||
addPairingCharacteristic:NO
|
||||
shouldAcceptSocketHandler:^BOOL(GNSSocket *socket) {
|
||||
_receivedSocket = socket;
|
||||
socket.delegate = _socketDelegateMock;
|
||||
return self->_shouldAcceptSocket;
|
||||
}];
|
||||
XCTAssertEqualObjects(_peripheralServiceManager.serviceUUID, _serviceUUID);
|
||||
[_peripheralServiceManager addedToPeripheralManager:_peripheralManagerMock
|
||||
bleServiceAddedCompletion:nil];
|
||||
XCTAssertEqual(GNSBluetoothServiceStateNotAdded, _peripheralServiceManager.cbServiceState);
|
||||
XCTAssertNil(_peripheralServiceManager.cbService);
|
||||
[_peripheralServiceManager willAddCBService];
|
||||
XCTAssertNotNil(_peripheralServiceManager.cbService);
|
||||
XCTAssertEqual(GNSBluetoothServiceStateAddInProgress, _peripheralServiceManager.cbServiceState);
|
||||
[_peripheralServiceManager didAddCBServiceWithError:nil];
|
||||
XCTAssertEqual(GNSBluetoothServiceStateAdded, _peripheralServiceManager.cbServiceState);
|
||||
}
|
||||
|
||||
- (void)tearDown {
|
||||
OCMVerifyAll(_peripheralManagerMock);
|
||||
OCMVerifyAll(_cbPeripheralManagerMock);
|
||||
OCMVerifyAll(_socketDelegateMock);
|
||||
for (id mock in _mocksToVerify) {
|
||||
OCMVerifyAll(mock);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testServiceManagerAdded {
|
||||
GNSPeripheralServiceManager *peripheralServiceManager = [[GNSPeripheralServiceManager alloc]
|
||||
initWithBleServiceUUID:_serviceUUID
|
||||
addPairingCharacteristic:NO
|
||||
shouldAcceptSocketHandler:^BOOL(GNSSocket *socket) {
|
||||
_receivedSocket = socket;
|
||||
return _shouldAcceptSocket;
|
||||
}];
|
||||
__block BOOL completionCalled = NO;
|
||||
[peripheralServiceManager addedToPeripheralManager:_peripheralManagerMock
|
||||
bleServiceAddedCompletion:^(NSError *error) {
|
||||
completionCalled = YES;
|
||||
}];
|
||||
XCTAssertEqual(peripheralServiceManager.peripheralManager, _peripheralManagerMock);
|
||||
XCTAssertFalse(completionCalled);
|
||||
[peripheralServiceManager willAddCBService];
|
||||
XCTAssertFalse(completionCalled);
|
||||
[peripheralServiceManager didAddCBServiceWithError:nil];
|
||||
XCTAssertTrue(completionCalled);
|
||||
}
|
||||
|
||||
- (void)testPeripheralServiceManagerRestored {
|
||||
GNSPeripheralServiceManager *manager =
|
||||
[[GNSPeripheralServiceManager alloc] initWithBleServiceUUID:_serviceUUID
|
||||
addPairingCharacteristic:NO
|
||||
shouldAcceptSocketHandler:^BOOL(GNSSocket *socket) {
|
||||
return NO;
|
||||
}];
|
||||
XCTAssertEqual(GNSBluetoothServiceStateNotAdded, manager.cbServiceState);
|
||||
|
||||
CBMutableCharacteristic *weaveOutgoingChar = OCMStrictClassMock([CBMutableCharacteristic class]);
|
||||
CBUUID *weaveOutgoingCharUUID = [CBUUID UUIDWithString:@"00000100-0004-1000-8000-001A11000102"];
|
||||
OCMStub([weaveOutgoingChar UUID]).andReturn(weaveOutgoingCharUUID);
|
||||
CBMutableCharacteristic *weaveIncomingChar = OCMStrictClassMock([CBMutableCharacteristic class]);
|
||||
CBUUID *weaveIncomingCharUUID = [CBUUID UUIDWithString:@"00000100-0004-1000-8000-001A11000101"];
|
||||
OCMStub([weaveIncomingChar UUID]).andReturn(weaveIncomingCharUUID);
|
||||
|
||||
CBMutableService *cbService = OCMStrictClassMock([CBMutableService class]);
|
||||
OCMStub([cbService UUID]).andReturn(_serviceUUID);
|
||||
NSArray *restoredCharacteristics = @[ weaveOutgoingChar, weaveIncomingChar ];
|
||||
OCMStub([cbService characteristics]).andReturn(restoredCharacteristics);
|
||||
[manager restoredCBService:cbService];
|
||||
XCTAssertEqual(GNSBluetoothServiceStateAdded, manager.cbServiceState);
|
||||
XCTAssertEqual(manager.cbService, cbService);
|
||||
XCTAssertEqual(manager.weaveOutgoingCharacteristic, weaveOutgoingChar);
|
||||
XCTAssertEqual(manager.weaveIncomingCharacteristic, weaveIncomingChar);
|
||||
}
|
||||
|
||||
- (void)testDefaultAdvertisingValue {
|
||||
XCTAssertTrue(_peripheralServiceManager.isAdvertising);
|
||||
}
|
||||
|
||||
- (void)testSetAdvertisingValueToTrue {
|
||||
// Nothing should happen since it is already to YES
|
||||
_peripheralServiceManager.advertising = YES;
|
||||
}
|
||||
|
||||
- (void)testSetAdvertisingValueToFalse {
|
||||
OCMExpect([_peripheralManagerMock updateAdvertisedServices]);
|
||||
_peripheralServiceManager.advertising = NO;
|
||||
}
|
||||
|
||||
- (void)testCanProcessReadRequest {
|
||||
CBATTRequest *request = OCMStrictClassMock([CBATTRequest class]);
|
||||
OCMStubRecorder *recorder = OCMStub([request characteristic]);
|
||||
|
||||
recorder.andReturn(_peripheralServiceManager.weaveOutgoingCharacteristic);
|
||||
XCTAssertEqual([_peripheralServiceManager canProcessReadRequest:request],
|
||||
CBATTErrorReadNotPermitted);
|
||||
recorder.andReturn(_peripheralServiceManager.weaveIncomingCharacteristic);
|
||||
XCTAssertEqual([_peripheralServiceManager canProcessReadRequest:request],
|
||||
CBATTErrorReadNotPermitted);
|
||||
|
||||
OCMVerifyAll((id)request);
|
||||
}
|
||||
|
||||
- (void)testCanProcessReadRequestOnWrongCharacteristic {
|
||||
CBATTRequest *request = OCMStrictClassMock([CBATTRequest class]);
|
||||
CBUUID *uuid = [CBUUID UUIDWithNSUUID:[NSUUID UUID]];
|
||||
id characteristicMock = OCMStrictClassMock([CBMutableCharacteristic class]);
|
||||
OCMStub([characteristicMock UUID]).andReturn(uuid);
|
||||
OCMStub([request characteristic]).andReturn(characteristicMock);
|
||||
XCTAssertEqual([_peripheralServiceManager canProcessReadRequest:request],
|
||||
CBATTErrorAttributeNotFound);
|
||||
OCMVerifyAll((id)request);
|
||||
OCMVerifyAll(characteristicMock);
|
||||
}
|
||||
|
||||
- (void)testCanProcessWriteRequestOnOutgoingCharacteristic {
|
||||
CBATTRequest *request = OCMStrictClassMock([CBATTRequest class]);
|
||||
OCMStubRecorder *recorder = OCMStub([request characteristic]);
|
||||
|
||||
recorder.andReturn(_peripheralServiceManager.weaveOutgoingCharacteristic);
|
||||
XCTAssertEqual([_peripheralServiceManager canProcessWriteRequest:request],
|
||||
CBATTErrorWriteNotPermitted);
|
||||
OCMVerifyAll((id)request);
|
||||
}
|
||||
|
||||
- (void)testCanProcessWriteRequestOnIncomingCharacteristic {
|
||||
CBATTRequest *request = OCMStrictClassMock([CBATTRequest class]);
|
||||
OCMStubRecorder *recorder = OCMStub([request characteristic]);
|
||||
|
||||
recorder.andReturn(_peripheralServiceManager.weaveIncomingCharacteristic);
|
||||
XCTAssertEqual([_peripheralServiceManager canProcessWriteRequest:request], CBATTErrorSuccess);
|
||||
OCMVerifyAll((id)request);
|
||||
}
|
||||
|
||||
- (void)testCanProcessWriteRequestOnWrongCharacteristic {
|
||||
CBATTRequest *request = OCMStrictClassMock([CBATTRequest class]);
|
||||
CBUUID *uuid = [CBUUID UUIDWithNSUUID:[NSUUID UUID]];
|
||||
id characteristicMock = OCMStrictClassMock([CBMutableCharacteristic class]);
|
||||
OCMStub([characteristicMock UUID]).andReturn(uuid);
|
||||
OCMStub([request characteristic]).andReturn(characteristicMock);
|
||||
XCTAssertEqual([_peripheralServiceManager canProcessWriteRequest:request],
|
||||
CBATTErrorAttributeNotFound);
|
||||
OCMVerifyAll((id)request);
|
||||
OCMVerifyAll(characteristicMock);
|
||||
}
|
||||
|
||||
- (CBCentral *)setupRequest:(id)request
|
||||
withCharacteristic:(CBMutableCharacteristic *)characteristic
|
||||
central:(CBCentral *)central {
|
||||
if (!central) {
|
||||
NSUUID *identifier = [NSUUID UUID];
|
||||
central = OCMStrictClassMock([CBCentral class]);
|
||||
OCMStub([central identifier]).andReturn(identifier);
|
||||
OCMStub([central maximumUpdateValueLength]).andReturn(_centralMaximumUpdateValueLength);
|
||||
[_mocksToVerify addObject:central];
|
||||
}
|
||||
OCMStub([request characteristic]).andReturn(characteristic);
|
||||
OCMStub([request central]).andReturn(central);
|
||||
return central;
|
||||
}
|
||||
|
||||
- (void)testProcessEmptyData {
|
||||
NSMutableData *data = [NSMutableData data];
|
||||
CBATTRequest *request = OCMStrictClassMock([CBATTRequest class]);
|
||||
OCMStub([request value]).andReturn(data);
|
||||
|
||||
[self setupRequest:request
|
||||
withCharacteristic:_peripheralServiceManager.weaveIncomingCharacteristic
|
||||
central:nil];
|
||||
[_peripheralServiceManager processWriteRequest:request];
|
||||
// should not crash, should do nothing.
|
||||
OCMVerifyAll((id)request);
|
||||
}
|
||||
|
||||
#pragma mark - Characteristics
|
||||
|
||||
- (void)testPairingChar {
|
||||
GNSPeripheralServiceManager *manager =
|
||||
[[GNSPeripheralServiceManager alloc] initWithBleServiceUUID:_serviceUUID
|
||||
addPairingCharacteristic:YES
|
||||
shouldAcceptSocketHandler:^BOOL(GNSSocket *socket) {
|
||||
return NO;
|
||||
}];
|
||||
[manager willAddCBService];
|
||||
[manager didAddCBServiceWithError:nil];
|
||||
CBMutableCharacteristic *pairingCharacteristic = manager.pairingCharacteristic;
|
||||
XCTAssertNotNil(pairingCharacteristic);
|
||||
XCTAssertEqualObjects(pairingCharacteristic.UUID.UUIDString,
|
||||
@"17836FBD-8C6A-4B81-83CE-8560629E834B",
|
||||
@"Wrong pairing characteristic UUID.");
|
||||
XCTAssertEqual(pairingCharacteristic.properties, CBCharacteristicPropertyRead,
|
||||
@"Wrong property for pairing characteristic.");
|
||||
XCTAssertEqual(pairingCharacteristic.permissions, CBAttributePermissionsReadEncryptionRequired,
|
||||
@"Wrong permission for pairing characteristic.");
|
||||
}
|
||||
|
||||
- (void)testNoPairingChar {
|
||||
GNSPeripheralServiceManager *manager =
|
||||
[[GNSPeripheralServiceManager alloc] initWithBleServiceUUID:_serviceUUID
|
||||
addPairingCharacteristic:NO
|
||||
shouldAcceptSocketHandler:^BOOL(GNSSocket *socket) {
|
||||
return NO;
|
||||
}];
|
||||
[manager willAddCBService];
|
||||
[manager didAddCBServiceWithError:nil];
|
||||
XCTAssertNil(manager.pairingCharacteristic);
|
||||
}
|
||||
|
||||
- (void)testOutgoingChar {
|
||||
CBMutableCharacteristic *weaveOutgoingCharacteristic =
|
||||
_peripheralServiceManager.weaveOutgoingCharacteristic;
|
||||
XCTAssertNotNil(weaveOutgoingCharacteristic);
|
||||
XCTAssertEqualObjects(weaveOutgoingCharacteristic.UUID.UUIDString,
|
||||
@"00000100-0004-1000-8000-001A11000102",
|
||||
@"Wrong weave outgoing characteristic UUID.");
|
||||
XCTAssertEqual(weaveOutgoingCharacteristic.properties, CBCharacteristicPropertyIndicate,
|
||||
@"Wrong property for weave outgoing characteristic.");
|
||||
XCTAssertEqual(weaveOutgoingCharacteristic.permissions, CBAttributePermissionsReadable,
|
||||
@"Wrong permission for weave outgoing characteristic.");
|
||||
}
|
||||
|
||||
- (void)testIncomingChar {
|
||||
CBMutableCharacteristic *weaveIncomingCharacteristic =
|
||||
_peripheralServiceManager.weaveIncomingCharacteristic;
|
||||
XCTAssertNotNil(weaveIncomingCharacteristic);
|
||||
XCTAssertEqualObjects(weaveIncomingCharacteristic.UUID.UUIDString,
|
||||
@"00000100-0004-1000-8000-001A11000101",
|
||||
@"Wrong weave incoming characteristic UUID.");
|
||||
XCTAssertEqual(weaveIncomingCharacteristic.properties, CBCharacteristicPropertyWrite,
|
||||
@"Wrong property for weave incoming characteristic.");
|
||||
XCTAssertEqual(weaveIncomingCharacteristic.permissions, CBAttributePermissionsWriteable,
|
||||
@"Wrong permission for weave incoming characteristic.");
|
||||
}
|
||||
|
||||
#pragma mark - Socket connection
|
||||
|
||||
- (void)checkOpenSocketWithShouldAccept:(BOOL)shouldAccept {
|
||||
[self checkOpenSocketWithShouldAccept:shouldAccept central:nil];
|
||||
}
|
||||
|
||||
- (void)checkOpenSocketWithShouldAccept:(BOOL)shouldAccept
|
||||
central:(CBCentral *)central {
|
||||
CBMutableCharacteristic *characteristic;
|
||||
NSMutableData *data = [NSMutableData data];
|
||||
_shouldAcceptSocket = shouldAccept;
|
||||
|
||||
// This is necessary to ensure that negotiated packet size is |_packetSize|. It should be set here
|
||||
// and in the connection request packet.
|
||||
_centralMaximumUpdateValueLength = _packetSize;
|
||||
characteristic = _peripheralServiceManager.weaveIncomingCharacteristic;
|
||||
GNSWeaveConnectionRequestPacket *connectionRequest =
|
||||
[[GNSWeaveConnectionRequestPacket alloc] initWithMinVersion:1
|
||||
maxVersion:1
|
||||
maxPacketSize:_packetSize
|
||||
data:nil];
|
||||
[data appendData:[connectionRequest serialize]];
|
||||
XCTAssertNotNil(characteristic);
|
||||
CBATTRequest *request = OCMStrictClassMock([CBATTRequest class]);
|
||||
OCMStub([request value]).andReturn(data);
|
||||
[self setupRequest:request
|
||||
withCharacteristic:characteristic
|
||||
central:central];
|
||||
__block GNSUpdateValueHandler updateValueHandler = nil;
|
||||
// Nothing is sent when the socket is refused in the Weave protocol.
|
||||
if (!shouldAccept) {
|
||||
return;
|
||||
}
|
||||
OCMExpect([_peripheralManagerMock
|
||||
updateOutgoingCharOnSocket:[OCMArg checkWithBlock:^BOOL(id obj) {
|
||||
// The socket has not been created yet. So the socket check has to be done with a block.
|
||||
return _receivedSocket == obj;
|
||||
}]
|
||||
withHandler:[OCMArg checkWithBlock:^(id handler) {
|
||||
updateValueHandler = [handler copy];
|
||||
return YES;
|
||||
}]]);
|
||||
[_peripheralServiceManager processWriteRequest:request];
|
||||
XCTAssertNotNil(updateValueHandler);
|
||||
if (!updateValueHandler) {
|
||||
return;
|
||||
}
|
||||
NSMutableData *expectedData = [NSMutableData data];
|
||||
GNSWeaveConnectionConfirmPacket *connectionConfirm =
|
||||
[[GNSWeaveConnectionConfirmPacket alloc] initWithVersion:1 packetSize:_packetSize data:nil];
|
||||
[expectedData appendData:[connectionConfirm serialize]];
|
||||
OCMExpect(
|
||||
[_peripheralManagerMock updateOutgoingCharacteristic:expectedData onSocket:_receivedSocket])
|
||||
.andReturn(YES);
|
||||
XCTAssertNotNil(_receivedSocket);
|
||||
XCTAssertFalse(_receivedSocket.isConnected);
|
||||
if (shouldAccept) {
|
||||
OCMExpect([_socketDelegateMock socketDidConnect:_receivedSocket]);
|
||||
}
|
||||
updateValueHandler(_peripheralManagerMock);
|
||||
XCTAssertEqual(_receivedSocket.isConnected, shouldAccept);
|
||||
OCMVerifyAll((id)request);
|
||||
}
|
||||
|
||||
- (void)testRefuseSocket {
|
||||
[self checkOpenSocketWithShouldAccept:NO];
|
||||
}
|
||||
|
||||
- (void)testAcceptSocket {
|
||||
[self checkOpenSocketWithShouldAccept:YES];
|
||||
}
|
||||
|
||||
- (void)testTwoConnectionRequests {
|
||||
[self checkOpenSocketWithShouldAccept:YES];
|
||||
GNSSocket *firstSocket = _receivedSocket;
|
||||
OCMExpect([_socketDelegateMock socket:firstSocket
|
||||
didDisconnectWithError:[OCMArg checkWithBlock:^BOOL(NSError *error) {
|
||||
return [error.domain isEqualToString:kGNSSocketsErrorDomain] &&
|
||||
error.code == GNSErrorNewInviteToConnectReceived;
|
||||
}]]);
|
||||
OCMExpect([_peripheralManagerMock socketDidDisconnect:firstSocket]);
|
||||
[self checkOpenSocketWithShouldAccept:YES
|
||||
central:firstSocket.peerAsCentral];
|
||||
XCTAssertNotEqual(firstSocket, _receivedSocket);
|
||||
XCTAssertFalse(firstSocket.isConnected);
|
||||
}
|
||||
|
||||
#pragma mark - Socket receive data
|
||||
|
||||
- (NSData *)generateDataWithSize:(uint32_t)length {
|
||||
NSMutableData *result = [NSMutableData dataWithCapacity:length];
|
||||
unsigned char byte = 0;
|
||||
for (NSInteger ii = 0; ii < length; ii++) {
|
||||
[result appendBytes:&byte length:sizeof(byte)];
|
||||
byte++;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
- (void)testReceiveEmptyMessage {
|
||||
[self checkOpenSocketWithShouldAccept:YES];
|
||||
|
||||
NSData *expectedMessage = [self generateDataWithSize:0];
|
||||
OCMExpect([_socketDelegateMock socket:_receivedSocket didReceiveData:expectedMessage]);
|
||||
|
||||
GNSWeaveDataPacket *dataPacket =
|
||||
[[GNSWeaveDataPacket alloc] initWithPacketCounter:_receivedSocket.receivePacketCounter
|
||||
firstPacket:YES
|
||||
lastPacket:YES
|
||||
data:expectedMessage];
|
||||
CBATTRequest *request = OCMStrictClassMock([CBATTRequest class]);
|
||||
OCMStub([request value]).andReturn([dataPacket serialize]);
|
||||
[self setupRequest:request
|
||||
withCharacteristic:_peripheralServiceManager.weaveIncomingCharacteristic
|
||||
central:_receivedSocket.peerAsCentral];
|
||||
[_peripheralServiceManager processWriteRequest:request];
|
||||
XCTAssertTrue(_receivedSocket.isConnected);
|
||||
OCMVerifyAll((id)request);
|
||||
}
|
||||
|
||||
- (void)simulateReceiveMessageWithSize:(uint32_t)size
|
||||
packetSize:(NSUInteger)packetSize
|
||||
counter:(UInt8)counter {
|
||||
NSData *expectedMessage = [self generateDataWithSize:size];
|
||||
OCMExpect([_socketDelegateMock socket:_receivedSocket didReceiveData:expectedMessage]);
|
||||
|
||||
NSUInteger offset = 0;
|
||||
UInt8 receivePacketCounter = counter;
|
||||
while (offset < size) {
|
||||
GNSWeaveDataPacket *dataPacket =
|
||||
[GNSWeaveDataPacket dataPacketWithPacketCounter:receivePacketCounter
|
||||
packetSize:packetSize
|
||||
data:expectedMessage
|
||||
offset:&offset];
|
||||
CBATTRequest *request = OCMStrictClassMock([CBATTRequest class]);
|
||||
OCMStub([request value]).andReturn([dataPacket serialize]);
|
||||
[self setupRequest:request
|
||||
withCharacteristic:_peripheralServiceManager.weaveIncomingCharacteristic
|
||||
central:_receivedSocket.peerAsCentral];
|
||||
[_peripheralServiceManager processWriteRequest:request];
|
||||
XCTAssertTrue(_receivedSocket.isConnected);
|
||||
OCMVerifyAll((id)request);
|
||||
receivePacketCounter = (receivePacketCounter + 1) % kGNSMaxPacketCounterValue;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testReceiveSinglePacketMessage {
|
||||
_packetSize = 100;
|
||||
[self checkOpenSocketWithShouldAccept:YES];
|
||||
[self simulateReceiveMessageWithSize:99
|
||||
packetSize:_packetSize
|
||||
counter:_receivedSocket.receivePacketCounter];
|
||||
}
|
||||
|
||||
- (void)testReceiveTwoSinglePacketMessages {
|
||||
_packetSize = 100;
|
||||
[self checkOpenSocketWithShouldAccept:YES];
|
||||
[self simulateReceiveMessageWithSize:1
|
||||
packetSize:_packetSize
|
||||
counter:_receivedSocket.receivePacketCounter];
|
||||
[self simulateReceiveMessageWithSize:98
|
||||
packetSize:_packetSize
|
||||
counter:_receivedSocket.receivePacketCounter];
|
||||
}
|
||||
|
||||
- (void)testReceiveMultiplePacketMessage {
|
||||
_packetSize = 100;
|
||||
[self checkOpenSocketWithShouldAccept:YES];
|
||||
[self simulateReceiveMessageWithSize:101
|
||||
packetSize:_packetSize
|
||||
counter:_receivedSocket.receivePacketCounter];
|
||||
}
|
||||
|
||||
- (void)testReceiveSeveralMultiplePacketMessage {
|
||||
_packetSize = 100;
|
||||
[self checkOpenSocketWithShouldAccept:YES];
|
||||
[self simulateReceiveMessageWithSize:101
|
||||
packetSize:_packetSize
|
||||
counter:_receivedSocket.receivePacketCounter];
|
||||
[self simulateReceiveMessageWithSize:198
|
||||
packetSize:_packetSize
|
||||
counter:_receivedSocket.receivePacketCounter];
|
||||
[self simulateReceiveMessageWithSize:1000
|
||||
packetSize:_packetSize
|
||||
counter:_receivedSocket.receivePacketCounter];
|
||||
}
|
||||
|
||||
#pragma mark - Socket send data
|
||||
|
||||
- (BOOL)simulateSendMessageWithExpectedData:(NSData *)expectedData
|
||||
packetSize:(NSUInteger)packetSize
|
||||
completion:(GNSErrorHandler)completion {
|
||||
BOOL sendHasBeenCompleted = YES;
|
||||
// The expectedData.length divided by the amount of data that fits a packet, rounded up.
|
||||
NSUInteger expectedPacketNumber =
|
||||
(expectedData.length / (packetSize - 1)) + (expectedData.length % (packetSize - 1) != 0);
|
||||
if (expectedData.length == 0) {
|
||||
expectedPacketNumber = 1;
|
||||
}
|
||||
_packetSize = packetSize;
|
||||
|
||||
[self checkOpenSocketWithShouldAccept:YES];
|
||||
UInt8 sendPacketCounter = _receivedSocket.sendPacketCounter;
|
||||
|
||||
__block GNSUpdateValueHandler updateValueHandler = nil;
|
||||
OCMStub([_peripheralManagerMock updateOutgoingCharOnSocket:_receivedSocket
|
||||
withHandler:[OCMArg checkWithBlock:^(id obj) {
|
||||
updateValueHandler = obj;
|
||||
return YES;
|
||||
}]]);
|
||||
// Send the data
|
||||
[_receivedSocket sendData:expectedData progressHandler:nil completion:completion];
|
||||
XCTAssertNotNil(updateValueHandler);
|
||||
if (!updateValueHandler) {
|
||||
return NO;
|
||||
}
|
||||
__block NSData *packetSent = nil;
|
||||
OCMStub([_peripheralManagerMock
|
||||
updateOutgoingCharacteristic:[OCMArg checkWithBlock:^BOOL(id obj) {
|
||||
packetSent = obj;
|
||||
return YES;
|
||||
}]
|
||||
onSocket:_receivedSocket])
|
||||
.andReturn(YES);
|
||||
|
||||
NSMutableData *sentData = [NSMutableData data];
|
||||
for (NSUInteger i = 0; i < expectedPacketNumber; i++) {
|
||||
// Cleanup |updateValueHandler| before calling it. So a new handler can be received if needed.
|
||||
GNSUpdateValueHandler tmpUpdateValueHandler = updateValueHandler;
|
||||
updateValueHandler = nil;
|
||||
XCTAssertNotNil(tmpUpdateValueHandler);
|
||||
tmpUpdateValueHandler(_peripheralManagerMock);
|
||||
NSError *error = nil;
|
||||
XCTAssertLessThanOrEqual(packetSent.length, packetSize);
|
||||
GNSWeavePacket *packet = [GNSWeavePacket parseData:packetSent error:&error];
|
||||
XCTAssertNil(error);
|
||||
XCTAssertNotNil(packet);
|
||||
XCTAssertTrue([packet isKindOfClass:[GNSWeaveDataPacket class]]);
|
||||
GNSWeaveDataPacket *dataPacket = (GNSWeaveDataPacket *)packet;
|
||||
NSLog(@"packetCounter = %d", dataPacket.packetCounter);
|
||||
XCTAssertEqual(dataPacket.packetCounter, sendPacketCounter);
|
||||
sendPacketCounter = (sendPacketCounter + 1) % kGNSMaxPacketCounterValue;
|
||||
if (i == 0) {
|
||||
XCTAssertTrue(dataPacket.isFirstPacket);
|
||||
} else {
|
||||
XCTAssertFalse(dataPacket.isFirstPacket);
|
||||
}
|
||||
[sentData appendData:dataPacket.data];
|
||||
if (i == expectedPacketNumber - 1) {
|
||||
XCTAssertTrue(dataPacket.isLastPacket);
|
||||
} else {
|
||||
XCTAssertFalse(dataPacket.isLastPacket);
|
||||
}
|
||||
}
|
||||
XCTAssertEqual((int)_receivedSocket.sendPacketCounter, (int)sendPacketCounter);
|
||||
if (sendHasBeenCompleted) {
|
||||
XCTAssertEqualObjects(sentData, expectedData);
|
||||
}
|
||||
return sendHasBeenCompleted;
|
||||
}
|
||||
|
||||
- (void)simulateMessageWithSize:(uint32_t)size packetSize:(NSUInteger)packetSize {
|
||||
__block NSInteger completionCalled = 0;
|
||||
NSData *expectedData = [self generateDataWithSize:size];
|
||||
BOOL completed = [self simulateSendMessageWithExpectedData:expectedData
|
||||
packetSize:packetSize
|
||||
completion:^(NSError *error) {
|
||||
XCTAssertNil(error);
|
||||
completionCalled++;
|
||||
}];
|
||||
XCTAssertTrue(completed);
|
||||
XCTAssertEqual(completionCalled, 1);
|
||||
}
|
||||
|
||||
- (void)testSendEmptyMessage {
|
||||
[self simulateMessageWithSize:0 packetSize:30];
|
||||
}
|
||||
|
||||
// The header has 1 byte.
|
||||
- (void)testSendSinglePacketMessage {
|
||||
[self simulateMessageWithSize:29 packetSize:30];
|
||||
}
|
||||
|
||||
// The header has 1 byte.
|
||||
- (void)testSendTwoPacketMessage {
|
||||
[self simulateMessageWithSize:30 packetSize:30];
|
||||
}
|
||||
|
||||
- (void)testSendMessagesWithSeveralPacketSizes {
|
||||
for (NSUInteger packetSize = 20; packetSize < 50; packetSize++) {
|
||||
[self simulateMessageWithSize:200 packetSize:packetSize];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testSendMessagesWithSeveralSizes {
|
||||
for (uint32_t size = 100; size < 1000; size += 50) {
|
||||
[self simulateMessageWithSize:size packetSize:120];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Socket disconnect
|
||||
|
||||
- (void)testUnsubscribeToDisconnect {
|
||||
[self checkOpenSocketWithShouldAccept:YES];
|
||||
XCTAssertTrue(_receivedSocket.isConnected);
|
||||
// Unsubscribing to the incoming characteristic: nothing should happen.
|
||||
[_peripheralServiceManager central:_receivedSocket.peerAsCentral
|
||||
didUnsubscribeFromCharacteristic:_peripheralServiceManager.weaveIncomingCharacteristic];
|
||||
XCTAssertTrue(_receivedSocket.isConnected);
|
||||
// Unsubscribing to the outgoing characteristic: the socket should be disconnected.
|
||||
OCMExpect([_socketDelegateMock socket:_receivedSocket didDisconnectWithError:nil]);
|
||||
OCMExpect([_peripheralManagerMock socketDidDisconnect:_receivedSocket]);
|
||||
[_peripheralServiceManager central:_receivedSocket.peerAsCentral
|
||||
didUnsubscribeFromCharacteristic:_peripheralServiceManager.weaveOutgoingCharacteristic];
|
||||
XCTAssertFalse(_receivedSocket.isConnected);
|
||||
}
|
||||
|
||||
- (void)testDisconnect {
|
||||
[self checkOpenSocketWithShouldAccept:YES];
|
||||
__block GNSUpdateValueHandler updateValueHandler = nil;
|
||||
OCMExpect([_peripheralManagerMock
|
||||
updateOutgoingCharOnSocket:_receivedSocket
|
||||
withHandler:[OCMArg checkWithBlock:^(id handler) {
|
||||
updateValueHandler = handler;
|
||||
return YES;
|
||||
}]]);
|
||||
OCMExpect([_peripheralManagerMock socketDidDisconnect:_receivedSocket]);
|
||||
UInt8 sendPacketCounter = _receivedSocket.sendPacketCounter;
|
||||
[_receivedSocket disconnect];
|
||||
XCTAssertNotNil(updateValueHandler);
|
||||
if (!updateValueHandler) {
|
||||
return;
|
||||
}
|
||||
GNSWeaveErrorPacket *errorPacket =
|
||||
[[GNSWeaveErrorPacket alloc] initWithPacketCounter:sendPacketCounter];
|
||||
OCMExpect([_peripheralManagerMock updateOutgoingCharacteristic:[errorPacket serialize]
|
||||
onSocket:_receivedSocket])
|
||||
.andReturn(YES);
|
||||
OCMExpect([_socketDelegateMock socket:_receivedSocket didDisconnectWithError:nil]);
|
||||
XCTAssertEqual(updateValueHandler(_peripheralManagerMock), GNSOutgoingCharUpdateNoReschedule);
|
||||
XCTAssertFalse(_receivedSocket.isConnected);
|
||||
}
|
||||
|
||||
- (void)testDisconnectWithErrorToSendDisconnectPacket {
|
||||
[self checkOpenSocketWithShouldAccept:YES];
|
||||
__block GNSUpdateValueHandler updateValueHandler = nil;
|
||||
OCMExpect([_peripheralManagerMock
|
||||
updateOutgoingCharOnSocket:_receivedSocket
|
||||
withHandler:[OCMArg checkWithBlock:^(id handler) {
|
||||
updateValueHandler = handler;
|
||||
return YES;
|
||||
}]]);
|
||||
OCMExpect([_peripheralManagerMock socketDidDisconnect:_receivedSocket]);
|
||||
UInt8 sendPacketCounter = _receivedSocket.sendPacketCounter;
|
||||
[_receivedSocket disconnect];
|
||||
XCTAssertNotNil(updateValueHandler);
|
||||
if (!updateValueHandler) {
|
||||
return;
|
||||
}
|
||||
GNSWeaveErrorPacket *errorPacket =
|
||||
[[GNSWeaveErrorPacket alloc] initWithPacketCounter:sendPacketCounter];
|
||||
OCMExpect([_peripheralManagerMock updateOutgoingCharacteristic:[errorPacket serialize]
|
||||
onSocket:_receivedSocket])
|
||||
.andReturn(NO);
|
||||
XCTAssertEqual(updateValueHandler(_peripheralManagerMock), GNSOutgoingCharUpdateScheduleLater);
|
||||
XCTAssertTrue(_receivedSocket.isConnected);
|
||||
OCMExpect([_peripheralManagerMock updateOutgoingCharacteristic:[errorPacket serialize]
|
||||
onSocket:_receivedSocket])
|
||||
.andReturn(YES);
|
||||
OCMExpect([_socketDelegateMock socket:_receivedSocket didDisconnectWithError:nil]);
|
||||
XCTAssertEqual(updateValueHandler(_peripheralManagerMock), GNSOutgoingCharUpdateNoReschedule);
|
||||
XCTAssertFalse(_receivedSocket.isConnected);
|
||||
}
|
||||
|
||||
- (void)testSubscribeToOutgoingCharacteristic {
|
||||
id centralMock = OCMStrictClassMock([CBCentral class]);
|
||||
id characteristic = OCMStrictClassMock([CBMutableCharacteristic class]);
|
||||
CBUUID *uuid = _peripheralServiceManager.weaveOutgoingCharacteristic.UUID;
|
||||
OCMStub([characteristic UUID]).andReturn(uuid);
|
||||
OCMStub([_cbPeripheralManagerMock
|
||||
setDesiredConnectionLatency:CBPeripheralManagerConnectionLatencyLow
|
||||
forCentral:centralMock]);
|
||||
[_peripheralServiceManager central:centralMock didSubscribeToCharacteristic:characteristic];
|
||||
OCMVerifyAll(centralMock);
|
||||
}
|
||||
|
||||
@end
|
||||
+355
@@ -0,0 +1,355 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/ble/Sockets/Source/Shared/GNSSocket+Private.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/ble/Sockets/Source/Shared/GNSWeavePacket.h"
|
||||
#import "third_party/objective_c/ocmock/v3/Source/OCMock/OCMock.h"
|
||||
|
||||
@interface GNSSocketTest : XCTestCase {
|
||||
GNSSocket *_socket;
|
||||
id<GNSSocketOwner> _socketOwner;
|
||||
CBCentral *_centralPeerMock;
|
||||
id<GNSSocketDelegate> _socketDelegate;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation GNSSocketTest
|
||||
|
||||
- (void)setUp {
|
||||
_centralPeerMock = OCMStrictClassMock([CBCentral class]);
|
||||
NSUUID *identifier = [NSUUID UUID];
|
||||
OCMStub([_centralPeerMock identifier]).andReturn(identifier);
|
||||
_socketOwner = [OCMStrictProtocolMock(@protocol(GNSSocketOwner)) noRetainObjectArgs];
|
||||
_socket = [[GNSSocket alloc] initWithOwner:_socketOwner centralPeer:_centralPeerMock];
|
||||
_socketDelegate = [OCMStrictProtocolMock(@protocol(GNSSocketDelegate)) noRetainObjectArgs];
|
||||
_socket.delegate = _socketDelegate;
|
||||
XCTAssertFalse(_socket.connected);
|
||||
}
|
||||
|
||||
- (void)tearDown {
|
||||
__weak GNSSocket *weakSocket = _socket;
|
||||
OCMExpect([_socketOwner socketWillBeDeallocated:[OCMArg isNotNil]]);
|
||||
_socket = nil;
|
||||
XCTAssertNil(weakSocket);
|
||||
OCMVerifyAll((id)_centralPeerMock);
|
||||
OCMVerifyAll((id)_socketOwner);
|
||||
OCMVerifyAll((id)_socketDelegate);
|
||||
}
|
||||
|
||||
- (void)connectSocket {
|
||||
OCMExpect([_socketDelegate socketDidConnect:_socket]);
|
||||
[_socket didConnect];
|
||||
XCTAssertTrue(_socket.connected);
|
||||
}
|
||||
|
||||
- (void)testPeripheralPeer {
|
||||
CBPeripheral *peripheral = OCMStrictClassMock([CBPeripheral class]);
|
||||
GNSSocket *socket = [[GNSSocket alloc] initWithOwner:_socketOwner peripheralPeer:peripheral];
|
||||
XCTAssertEqual(socket.peerAsPeripheral, peripheral);
|
||||
OCMExpect([_socketOwner socketWillBeDeallocated:[OCMArg isNotNil]]);
|
||||
}
|
||||
|
||||
- (void)testCentralPeer {
|
||||
CBCentral *central = OCMStrictClassMock([CBCentral class]);
|
||||
GNSSocket *socket = [[GNSSocket alloc] initWithOwner:_socketOwner centralPeer:central];
|
||||
XCTAssertEqual(socket.peerAsCentral, central);
|
||||
OCMExpect([_socketOwner socketWillBeDeallocated:[OCMArg isNotNil]]);
|
||||
}
|
||||
|
||||
- (void)testDisconnect {
|
||||
[self connectSocket];
|
||||
OCMExpect([_socketOwner disconnectSocket:_socket]);
|
||||
[_socket disconnect];
|
||||
XCTAssertTrue(_socket.connected);
|
||||
OCMExpect([_socketDelegate socket:_socket didDisconnectWithError:nil]);
|
||||
[_socket didDisconnectWithError:nil];
|
||||
XCTAssertFalse(_socket.connected);
|
||||
}
|
||||
|
||||
- (void)testDisconnectWithError {
|
||||
[self connectSocket];
|
||||
OCMExpect([_socketOwner disconnectSocket:_socket]);
|
||||
[_socket disconnect];
|
||||
XCTAssertTrue(_socket.connected);
|
||||
NSError *error = [NSError errorWithDomain:@"domain" code:-42 userInfo:nil];
|
||||
OCMExpect([_socketDelegate socket:_socket didDisconnectWithError:error]);
|
||||
[_socket didDisconnectWithError:error];
|
||||
XCTAssertFalse(_socket.connected);
|
||||
}
|
||||
|
||||
#pragma mark - Receive Data
|
||||
|
||||
- (void)testReceiveDataWithOnePacket {
|
||||
[self connectSocket];
|
||||
XCTAssertFalse([_socket waitingForIncomingData]);
|
||||
NSData *data = [@"Some data to receive" dataUsingEncoding:NSUTF8StringEncoding];
|
||||
OCMExpect([_socketDelegate socket:_socket didReceiveData:data]);
|
||||
GNSWeaveDataPacket *packet =
|
||||
[[GNSWeaveDataPacket alloc] initWithPacketCounter:1 firstPacket:YES lastPacket:YES data:data];
|
||||
[_socket didReceiveIncomingWeaveDataPacket:packet];
|
||||
XCTAssertFalse([_socket waitingForIncomingData]);
|
||||
}
|
||||
|
||||
- (void)testReceiveDataWithTwoPackets {
|
||||
[self connectSocket];
|
||||
XCTAssertFalse([_socket waitingForIncomingData]);
|
||||
NSData *firstChunk = [@"Some data " dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSData *secondChunk = [@"to receive" dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSMutableData *totalData = [NSMutableData data];
|
||||
[totalData appendData:firstChunk];
|
||||
GNSWeaveDataPacket *firstPacket = [[GNSWeaveDataPacket alloc] initWithPacketCounter:1
|
||||
firstPacket:YES
|
||||
lastPacket:NO
|
||||
data:firstChunk];
|
||||
[_socket didReceiveIncomingWeaveDataPacket:firstPacket];
|
||||
XCTAssertTrue([_socket waitingForIncomingData]);
|
||||
OCMExpect([_socketDelegate socket:_socket didReceiveData:totalData]);
|
||||
[totalData appendData:secondChunk];
|
||||
GNSWeaveDataPacket *secondPacket = [[GNSWeaveDataPacket alloc] initWithPacketCounter:1
|
||||
firstPacket:NO
|
||||
lastPacket:YES
|
||||
data:secondChunk];
|
||||
[_socket didReceiveIncomingWeaveDataPacket:secondPacket];
|
||||
XCTAssertFalse([_socket waitingForIncomingData]);
|
||||
}
|
||||
|
||||
- (void)testReceiveDataWithOneDataPacketAndDisconnect {
|
||||
[self connectSocket];
|
||||
XCTAssertFalse([_socket waitingForIncomingData]);
|
||||
NSData *firstChunk = [@"Some data " dataUsingEncoding:NSUTF8StringEncoding];
|
||||
GNSWeaveDataPacket *firstPacket = [[GNSWeaveDataPacket alloc] initWithPacketCounter:1
|
||||
firstPacket:YES
|
||||
lastPacket:NO
|
||||
data:firstChunk];
|
||||
[_socket didReceiveIncomingWeaveDataPacket:firstPacket];
|
||||
XCTAssertTrue([_socket waitingForIncomingData]);
|
||||
OCMExpect([_socketDelegate socket:_socket didDisconnectWithError:nil]);
|
||||
[_socket didDisconnectWithError:nil];
|
||||
XCTAssertFalse(_socket.connected);
|
||||
XCTAssertFalse([_socket waitingForIncomingData]);
|
||||
}
|
||||
|
||||
#pragma mark - Send Data
|
||||
|
||||
- (void)testSendDataWithOnePacket {
|
||||
_socket.packetSize = 100;
|
||||
UInt8 sendPacketCounter = _socket.sendPacketCounter;
|
||||
[self connectSocket];
|
||||
NSData *data = [@"Some data to send" dataUsingEncoding:NSUTF8StringEncoding];
|
||||
OCMStub([_socketOwner socketMaximumUpdateValueLength:[self checkSocketBlock]]).andReturn(100);
|
||||
__block GNSUpdateValueHandler handler = nil;
|
||||
OCMExpect([_socketOwner socket:_socket
|
||||
addOutgoingCharUpdateHandler:[OCMArg checkWithBlock:^BOOL(id obj) {
|
||||
handler = obj;
|
||||
return YES;
|
||||
}]]);
|
||||
__block BOOL completionCalledCount = 0;
|
||||
__block BOOL progressHandlerCount = 0;
|
||||
[_socket sendData:data
|
||||
progressHandler:^void(float progress) {
|
||||
progressHandlerCount++;
|
||||
}
|
||||
completion:^(NSError *error) {
|
||||
XCTAssertNil(error);
|
||||
completionCalledCount++;
|
||||
}];
|
||||
XCTAssertTrue([_socket isSendOperationInProgress]);
|
||||
XCTAssertNotNil(handler);
|
||||
if (!handler) {
|
||||
return;
|
||||
}
|
||||
NSUInteger offset = 0;
|
||||
GNSWeaveDataPacket *packet = [GNSWeaveDataPacket dataPacketWithPacketCounter:sendPacketCounter
|
||||
packetSize:_socket.packetSize
|
||||
data:data
|
||||
offset:&offset];
|
||||
NSData *packetData = [packet serialize];
|
||||
OCMExpect([_socketOwner socket:_socket sendData:packetData]).andReturn(YES);
|
||||
XCTAssertEqual(handler(), GNSOutgoingCharUpdateNoReschedule);
|
||||
XCTAssertEqual(completionCalledCount, 1);
|
||||
XCTAssertEqual(progressHandlerCount, 1);
|
||||
XCTAssertFalse([_socket isSendOperationInProgress]);
|
||||
}
|
||||
|
||||
- (void)testSendDataWithTwoPackets {
|
||||
_socket.packetSize = 20;
|
||||
UInt8 sendPacketCounter = _socket.sendPacketCounter;
|
||||
[self connectSocket];
|
||||
NSData *data = [@"Some larger data to send" dataUsingEncoding:NSUTF8StringEncoding];
|
||||
OCMStub([_socketOwner socketMaximumUpdateValueLength:[self checkSocketBlock]]).andReturn(20);
|
||||
__block GNSUpdateValueHandler handler = nil;
|
||||
OCMExpect([_socketOwner socket:_socket
|
||||
addOutgoingCharUpdateHandler:[OCMArg checkWithBlock:^BOOL(id obj) {
|
||||
handler = obj;
|
||||
return YES;
|
||||
}]]);
|
||||
__block BOOL completionCalledCount = 0;
|
||||
[_socket sendData:data progressHandler:nil completion:^(NSError *error) {
|
||||
XCTAssertNil(error);
|
||||
completionCalledCount++;
|
||||
}];
|
||||
XCTAssertNotNil(handler);
|
||||
if (!handler) {
|
||||
return;
|
||||
}
|
||||
NSUInteger offset = 0;
|
||||
GNSWeaveDataPacket *firstPacket =
|
||||
[GNSWeaveDataPacket dataPacketWithPacketCounter:sendPacketCounter
|
||||
packetSize:_socket.packetSize
|
||||
data:data
|
||||
offset:&offset];
|
||||
NSData *firstPacketData = [firstPacket serialize];
|
||||
OCMExpect([_socketOwner socket:_socket sendData:firstPacketData]).andReturn(YES);
|
||||
OCMExpect([_socketOwner socket:_socket
|
||||
addOutgoingCharUpdateHandler:[OCMArg checkWithBlock:^BOOL(id obj) {
|
||||
handler = obj;
|
||||
return YES;
|
||||
}]]);
|
||||
XCTAssertEqual(handler(), GNSOutgoingCharUpdateNoReschedule);
|
||||
XCTAssertNotNil(handler);
|
||||
if (!handler) {
|
||||
return;
|
||||
}
|
||||
XCTAssertEqual(completionCalledCount, 0);
|
||||
|
||||
GNSWeaveDataPacket *secondPacket =
|
||||
[GNSWeaveDataPacket dataPacketWithPacketCounter:sendPacketCounter + 1
|
||||
packetSize:_socket.packetSize
|
||||
data:data
|
||||
offset:&offset];
|
||||
NSData *secondPacketData = [secondPacket serialize];
|
||||
OCMExpect([_socketOwner socket:_socket sendData:secondPacketData]).andReturn(YES);
|
||||
XCTAssertEqual(handler(), GNSOutgoingCharUpdateNoReschedule);
|
||||
XCTAssertEqual(completionCalledCount, 1);
|
||||
XCTAssertFalse([_socket isSendOperationInProgress]);
|
||||
}
|
||||
|
||||
- (void)testSendDataWithBluetoothRetry {
|
||||
_socket.packetSize = 100;
|
||||
UInt8 sentPacketCounter = _socket.sendPacketCounter;
|
||||
[self connectSocket];
|
||||
NSData *data = [@"Some data to send" dataUsingEncoding:NSUTF8StringEncoding];
|
||||
OCMStub([_socketOwner socketMaximumUpdateValueLength:[self checkSocketBlock]]).andReturn(100);
|
||||
__block GNSUpdateValueHandler handler = nil;
|
||||
OCMExpect([_socketOwner socket:_socket
|
||||
addOutgoingCharUpdateHandler:[OCMArg checkWithBlock:^BOOL(id obj) {
|
||||
handler = obj;
|
||||
return YES;
|
||||
}]]);
|
||||
__block BOOL completionCalledCount = 0;
|
||||
[_socket sendData:data progressHandler:nil completion:^(NSError *error) {
|
||||
XCTAssertNil(error);
|
||||
completionCalledCount++;
|
||||
}];
|
||||
XCTAssertTrue([_socket isSendOperationInProgress]);
|
||||
XCTAssertNotNil(handler);
|
||||
if (!handler) {
|
||||
return;
|
||||
}
|
||||
NSUInteger offset = 0;
|
||||
GNSWeaveDataPacket *packet = [GNSWeaveDataPacket dataPacketWithPacketCounter:sentPacketCounter
|
||||
packetSize:_socket.packetSize
|
||||
data:data
|
||||
offset:&offset];
|
||||
NSData *packetData = [packet serialize];
|
||||
OCMExpect([_socketOwner socket:_socket sendData:packetData]).andReturn(NO);
|
||||
XCTAssertEqual(handler(), GNSOutgoingCharUpdateScheduleLater);
|
||||
XCTAssertEqual(completionCalledCount, 0);
|
||||
XCTAssertTrue([_socket isSendOperationInProgress]);
|
||||
OCMExpect([_socketOwner socket:_socket sendData:packetData]).andReturn(YES);
|
||||
XCTAssertEqual(handler(), GNSOutgoingCharUpdateNoReschedule);
|
||||
XCTAssertEqual(completionCalledCount, 1);
|
||||
XCTAssertFalse([_socket isSendOperationInProgress]);
|
||||
}
|
||||
|
||||
- (void)testDisconnectAndSendDataWithOneChunk {
|
||||
[self connectSocket];
|
||||
OCMExpect([_socketDelegate socket:_socket didDisconnectWithError:nil]);
|
||||
[_socket didDisconnectWithError:nil];
|
||||
XCTAssertFalse(_socket.connected);
|
||||
NSData *data = [@"Some data to send" dataUsingEncoding:NSUTF8StringEncoding];
|
||||
OCMStub([_socketOwner socketMaximumUpdateValueLength:[self checkSocketBlock]]).andReturn(100);
|
||||
__block BOOL completionCalledCount = 0;
|
||||
[_socket sendData:data progressHandler:nil completion:^(NSError *error) {
|
||||
XCTAssertNotNil(error);
|
||||
XCTAssertEqualObjects(error.domain, kGNSSocketsErrorDomain);
|
||||
XCTAssertEqual(error.code, GNSErrorNoConnection);
|
||||
completionCalledCount++;
|
||||
}];
|
||||
XCTAssertEqual(completionCalledCount, 1);
|
||||
XCTAssertFalse([_socket isSendOperationInProgress]);
|
||||
}
|
||||
|
||||
- (void)testSendDataWithTwoChunksAndDisconnectBetweenChunk {
|
||||
_socket.packetSize = 20;
|
||||
UInt8 sentPacketCounter = _socket.sendPacketCounter;
|
||||
[self connectSocket];
|
||||
NSData *data = [@"Some larger data to send" dataUsingEncoding:NSUTF8StringEncoding];
|
||||
OCMStub([_socketOwner socketMaximumUpdateValueLength:[self checkSocketBlock]]).andReturn(20);
|
||||
__block GNSUpdateValueHandler handler = nil;
|
||||
OCMExpect([_socketOwner socket:_socket
|
||||
addOutgoingCharUpdateHandler:[OCMArg checkWithBlock:^BOOL(id obj) {
|
||||
handler = obj;
|
||||
return YES;
|
||||
}]]);
|
||||
__block BOOL completionCalledCount = 0;
|
||||
[_socket sendData:data progressHandler:nil completion:^(NSError *error) {
|
||||
XCTAssertNotNil(error);
|
||||
XCTAssertEqualObjects(error.domain, kGNSSocketsErrorDomain);
|
||||
XCTAssertEqual(error.code, GNSErrorNoConnection);
|
||||
completionCalledCount++;
|
||||
}];
|
||||
XCTAssertNotNil(handler);
|
||||
if (!handler) {
|
||||
return;
|
||||
}
|
||||
NSUInteger offset = 0;
|
||||
GNSWeaveDataPacket *packet = [GNSWeaveDataPacket dataPacketWithPacketCounter:sentPacketCounter
|
||||
packetSize:_socket.packetSize
|
||||
data:data
|
||||
offset:&offset];
|
||||
NSData *packetData = [packet serialize];
|
||||
OCMExpect([_socketOwner socket:_socket sendData:packetData]).andReturn(YES);
|
||||
OCMExpect([_socketOwner socket:_socket
|
||||
addOutgoingCharUpdateHandler:[OCMArg checkWithBlock:^BOOL(id obj) {
|
||||
handler = obj;
|
||||
return YES;
|
||||
}]]);
|
||||
XCTAssertEqual(handler(), GNSOutgoingCharUpdateNoReschedule);
|
||||
XCTAssertNotNil(handler);
|
||||
if (!handler) {
|
||||
return;
|
||||
}
|
||||
XCTAssertEqual(completionCalledCount, 0);
|
||||
OCMExpect([_socketDelegate socket:_socket didDisconnectWithError:nil]);
|
||||
[_socket didDisconnectWithError:nil];
|
||||
XCTAssertFalse(_socket.connected);
|
||||
XCTAssertEqual(handler(), GNSOutgoingCharUpdateNoReschedule);
|
||||
XCTAssertEqual(completionCalledCount, 1);
|
||||
XCTAssertFalse([_socket isSendOperationInProgress]);
|
||||
}
|
||||
|
||||
#pragma mark - Helpers
|
||||
|
||||
- (id)checkSocketBlock {
|
||||
// This method create a OCMArg without retaining the parameter.
|
||||
__weak GNSSocket *weakSocket = _socket;
|
||||
return [OCMArg checkWithBlock:^BOOL(id object) {
|
||||
return object == weakSocket;
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
+310
@@ -0,0 +1,310 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/ble/Sockets/Source/Shared/GNSWeavePacket.h"
|
||||
|
||||
#import "internal/platform/implementation/ios/Mediums/ble/Sockets/Source/Shared/GNSUtils.h"
|
||||
|
||||
@interface GNSWeavePacketTest : XCTestCase {
|
||||
NSData *_largeNonEmptyData;
|
||||
NSData *_smallNonEmptyData;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation GNSWeavePacketTest
|
||||
|
||||
- (void)setUp {
|
||||
_largeNonEmptyData =
|
||||
[@"Some non-empty data data is larger than 20 bytes." dataUsingEncoding:NSUTF8StringEncoding];
|
||||
_smallNonEmptyData = [@"Small data" dataUsingEncoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
- (NSData *)weavePacketWithHeader:(UInt8)header data:(NSData *)data {
|
||||
NSMutableData *dataPacket = [[NSMutableData alloc] init];
|
||||
[dataPacket appendBytes:&header length:sizeof(header)];
|
||||
if (data != nil) {
|
||||
[dataPacket appendData:[data subdataWithRange:NSMakeRange(0, data.length)]];
|
||||
}
|
||||
return dataPacket;
|
||||
}
|
||||
|
||||
- (void)testParsePacketCounter {
|
||||
for (UInt8 packetCounter = 0; packetCounter < 8; packetCounter++) {
|
||||
UInt8 header = (packetCounter
|
||||
<< 4); // 0111 0000, the packetCounter value is contained on 5, 6, and 7 bits.
|
||||
NSError *error = nil;
|
||||
GNSWeavePacket *parsedPacket =
|
||||
[GNSWeavePacket parseData:[self weavePacketWithHeader:header data:_largeNonEmptyData]
|
||||
error:&error];
|
||||
XCTAssertNotNil(parsedPacket);
|
||||
XCTAssertEqual(packetCounter, parsedPacket.packetCounter);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testParseEmptyPacket {
|
||||
NSData *data = [NSData data];
|
||||
NSError *error = nil;
|
||||
GNSWeavePacket *parsedPacket = [GNSWeavePacket parseData:data error:&error];
|
||||
XCTAssertNil(parsedPacket);
|
||||
XCTAssertNotNil(error);
|
||||
XCTAssertEqual(error.domain, kGNSSocketsErrorDomain);
|
||||
XCTAssertEqual(error.code, GNSErrorParsingWeavePacketTooSmall);
|
||||
}
|
||||
|
||||
- (void)testParsingControlPacketTooLarge {
|
||||
UInt8 header = (1 << 7); // 1000 0000
|
||||
NSError *error = nil;
|
||||
GNSWeavePacket *parsedPacket =
|
||||
[GNSWeavePacket parseData:[self weavePacketWithHeader:header data:_largeNonEmptyData]
|
||||
error:&error];
|
||||
XCTAssertNil(parsedPacket);
|
||||
XCTAssertNotNil(error);
|
||||
XCTAssertEqual(error.domain, kGNSSocketsErrorDomain);
|
||||
XCTAssertEqual(error.code, GNSErrorParsingWeavePacketTooLarge);
|
||||
}
|
||||
|
||||
- (void)testParseFirstDataPacket {
|
||||
UInt8 header = (1 << 3); // 0000 1000, the packet counter is zero
|
||||
NSError *error = nil;
|
||||
GNSWeavePacket *parsedPacket =
|
||||
[GNSWeavePacket parseData:[self weavePacketWithHeader:header data:_largeNonEmptyData]
|
||||
error:&error];
|
||||
XCTAssertNotNil(parsedPacket);
|
||||
XCTAssertTrue([parsedPacket isKindOfClass:[GNSWeaveDataPacket class]]);
|
||||
GNSWeaveDataPacket *parsedDataPacket = (GNSWeaveDataPacket *)parsedPacket;
|
||||
XCTAssertTrue(parsedDataPacket.isFirstPacket);
|
||||
XCTAssertFalse(parsedDataPacket.isLastPacket);
|
||||
XCTAssertEqualObjects(parsedDataPacket.data, _largeNonEmptyData);
|
||||
}
|
||||
|
||||
- (void)testParseLastDataPacket {
|
||||
UInt8 header = (1 << 2); // 0000 0100, the packet counter is zero
|
||||
NSError *error = nil;
|
||||
GNSWeavePacket *parsedPacket =
|
||||
[GNSWeavePacket parseData:[self weavePacketWithHeader:header data:_largeNonEmptyData]
|
||||
error:&error];
|
||||
XCTAssertNotNil(parsedPacket);
|
||||
XCTAssertTrue([parsedPacket isKindOfClass:[GNSWeaveDataPacket class]]);
|
||||
GNSWeaveDataPacket *parsedDataPacket = (GNSWeaveDataPacket *)parsedPacket;
|
||||
XCTAssertFalse(parsedDataPacket.isFirstPacket);
|
||||
XCTAssertTrue(parsedDataPacket.isLastPacket);
|
||||
XCTAssertEqualObjects(parsedDataPacket.data, _largeNonEmptyData);
|
||||
}
|
||||
|
||||
- (void)testParseOtherDataPacket {
|
||||
UInt8 header = 0; // 0000 0000, the packet counter is zero
|
||||
NSError *error = nil;
|
||||
GNSWeavePacket *parsedPacket =
|
||||
[GNSWeavePacket parseData:[self weavePacketWithHeader:header data:_largeNonEmptyData]
|
||||
error:&error];
|
||||
XCTAssertNotNil(parsedPacket);
|
||||
XCTAssertTrue([parsedPacket isKindOfClass:[GNSWeaveDataPacket class]]);
|
||||
GNSWeaveDataPacket *parsedDataPacket = (GNSWeaveDataPacket *)parsedPacket;
|
||||
XCTAssertFalse(parsedDataPacket.isFirstPacket);
|
||||
XCTAssertFalse(parsedDataPacket.isLastPacket);
|
||||
XCTAssertEqualObjects(parsedDataPacket.data, _largeNonEmptyData);
|
||||
}
|
||||
|
||||
- (void)testBuildAndParseMultipleDataPackets {
|
||||
NSMutableData *receivedData = [[NSMutableData alloc] init];
|
||||
NSUInteger offset = 0;
|
||||
UInt8 packetCounter = 0;
|
||||
UInt16 packetSize = 20;
|
||||
BOOL isLastPacket = NO;
|
||||
while (offset < _largeNonEmptyData.length) {
|
||||
GNSWeaveDataPacket *dataPacket =
|
||||
[GNSWeaveDataPacket dataPacketWithPacketCounter:packetCounter
|
||||
packetSize:packetSize
|
||||
data:_largeNonEmptyData
|
||||
offset:&offset];
|
||||
XCTAssertNotNil(dataPacket);
|
||||
NSError *error = nil;
|
||||
GNSWeavePacket *parsedPacket = [GNSWeavePacket parseData:[dataPacket serialize] error:&error];
|
||||
XCTAssertNotNil(parsedPacket);
|
||||
XCTAssertTrue([parsedPacket isKindOfClass:[GNSWeaveDataPacket class]]);
|
||||
GNSWeaveDataPacket *parsedDataPacket = (GNSWeaveDataPacket *)parsedPacket;
|
||||
if (receivedData.length == 0) {
|
||||
XCTAssertTrue(parsedDataPacket.isFirstPacket);
|
||||
}
|
||||
[receivedData appendData:[parsedDataPacket.data
|
||||
subdataWithRange:NSMakeRange(0, parsedDataPacket.data.length)]];
|
||||
packetCounter++;
|
||||
isLastPacket = parsedDataPacket.isLastPacket;
|
||||
}
|
||||
XCTAssertTrue(isLastPacket);
|
||||
XCTAssertEqual(offset, _largeNonEmptyData.length);
|
||||
XCTAssertEqualObjects(receivedData, _largeNonEmptyData);
|
||||
XCTAssertEqualWithAccuracy(packetCounter,
|
||||
ceil(_largeNonEmptyData.length / (float)(packetSize - 1)), 0.1f);
|
||||
}
|
||||
|
||||
- (void)testBuildAndParseSingleDataPacket {
|
||||
NSUInteger offset = 0;
|
||||
UInt8 packetCounter = 0;
|
||||
UInt16 packetSize = 20;
|
||||
GNSWeaveDataPacket *dataPacket =
|
||||
[GNSWeaveDataPacket dataPacketWithPacketCounter:packetCounter
|
||||
packetSize:packetSize
|
||||
data:_smallNonEmptyData
|
||||
offset:&offset];
|
||||
|
||||
XCTAssertNotNil(dataPacket);
|
||||
NSError *error = nil;
|
||||
GNSWeavePacket *parsedPacket = [GNSWeavePacket parseData:[dataPacket serialize] error:&error];
|
||||
XCTAssertNotNil(parsedPacket);
|
||||
XCTAssertTrue([parsedPacket isKindOfClass:[GNSWeaveDataPacket class]]);
|
||||
GNSWeaveDataPacket *parsedDataPacket = (GNSWeaveDataPacket *)parsedPacket;
|
||||
XCTAssertTrue(parsedDataPacket.isLastPacket);
|
||||
XCTAssertTrue(parsedDataPacket.isFirstPacket);
|
||||
XCTAssertEqual(offset, _smallNonEmptyData.length);
|
||||
XCTAssertEqualObjects(dataPacket.data, _smallNonEmptyData);
|
||||
}
|
||||
|
||||
- (void)testParseErrorControlPacket {
|
||||
// 1000 0010, control packets have the MSB set and, error code is 2. The packet counter is 0.
|
||||
UInt8 header = (1 << 7) + 2;
|
||||
NSError *error = nil;
|
||||
GNSWeavePacket *packet =
|
||||
[GNSWeavePacket parseData:[self weavePacketWithHeader:header data:nil] error:&error];
|
||||
XCTAssertNotNil(packet);
|
||||
XCTAssertTrue([packet isKindOfClass:[GNSWeaveErrorPacket class]]);
|
||||
XCTAssertEqual(packet.packetCounter, 0);
|
||||
}
|
||||
|
||||
- (void)testBuildErrorControlPacket {
|
||||
UInt8 packetCounter = 7;
|
||||
GNSWeaveErrorPacket *errorPacket =
|
||||
[[GNSWeaveErrorPacket alloc] initWithPacketCounter:packetCounter];
|
||||
NSData *serializedPacket = [errorPacket serialize];
|
||||
UInt8 firstByte = *(UInt8 *)serializedPacket.bytes;
|
||||
// 1111 0010, control packet bit is set, counter is 7 (111) and error code is 2.
|
||||
XCTAssertEqual(firstByte, (1 << 7) + (7 << 4) + 2);
|
||||
|
||||
NSError *error = nil;
|
||||
GNSWeavePacket *packet = [GNSWeavePacket parseData:serializedPacket error:&error];
|
||||
XCTAssertNotNil(packet);
|
||||
XCTAssertTrue([packet isKindOfClass:[GNSWeaveErrorPacket class]]);
|
||||
XCTAssertEqual(packet.packetCounter, packetCounter);
|
||||
}
|
||||
|
||||
- (void)testParseConnectionRequestControlPacket {
|
||||
// 1000 0000, control packets have the most significat bit (MSB) set and, connection request code
|
||||
// is 0. The packet counter is 0.
|
||||
UInt8 header = (1 << 7) + 0;
|
||||
NSMutableData *payload = [[NSMutableData alloc] init];
|
||||
UInt16 minVersion = 0;
|
||||
UInt16 maxVersion = 3;
|
||||
UInt16 maxPacketSize = 200;
|
||||
|
||||
// The Weave protocol uses big-endian format for the multi-bytes types.
|
||||
UInt16 minVersionBigEndian = CFSwapInt16HostToBig(minVersion);
|
||||
UInt16 maxVersionBigEndian = CFSwapInt16HostToBig(maxVersion);
|
||||
UInt16 maxPacketSizeBigEndian = CFSwapInt16HostToBig(maxPacketSize);
|
||||
[payload appendBytes:&minVersionBigEndian length:sizeof(minVersionBigEndian)];
|
||||
[payload appendBytes:&maxVersionBigEndian length:sizeof(maxVersionBigEndian)];
|
||||
[payload appendBytes:&maxPacketSizeBigEndian length:sizeof(maxPacketSizeBigEndian)];
|
||||
NSError *error = nil;
|
||||
GNSWeavePacket *packet =
|
||||
[GNSWeavePacket parseData:[self weavePacketWithHeader:header data:payload] error:&error];
|
||||
XCTAssertNotNil(packet);
|
||||
XCTAssertTrue([packet isKindOfClass:[GNSWeaveConnectionRequestPacket class]]);
|
||||
XCTAssertEqual(packet.packetCounter, 0);
|
||||
GNSWeaveConnectionRequestPacket *connectionRequestPacket =
|
||||
(GNSWeaveConnectionRequestPacket *)packet;
|
||||
XCTAssertEqual(connectionRequestPacket.minVersion, minVersion);
|
||||
XCTAssertEqual(connectionRequestPacket.maxVersion, maxVersion);
|
||||
XCTAssertEqual(connectionRequestPacket.maxPacketSize, maxPacketSize);
|
||||
XCTAssertNil(connectionRequestPacket.data);
|
||||
}
|
||||
|
||||
- (void)testBuildConnectionRequestControlPacket {
|
||||
UInt16 minVersion = 0;
|
||||
UInt16 maxVersion = 3;
|
||||
UInt16 maxPacketSize = 200;
|
||||
NSData *smallPayload = [@"Payload" dataUsingEncoding:NSUTF8StringEncoding];
|
||||
GNSWeaveConnectionRequestPacket *requestPacket =
|
||||
[[GNSWeaveConnectionRequestPacket alloc] initWithMinVersion:minVersion
|
||||
maxVersion:maxVersion
|
||||
maxPacketSize:maxPacketSize
|
||||
data:smallPayload];
|
||||
NSData *serializedPacket = [requestPacket serialize];
|
||||
UInt8 firstByte = *(UInt8 *)serializedPacket.bytes;
|
||||
XCTAssertEqual(firstByte, (1 << 7)); // 1000 0000
|
||||
|
||||
NSError *error = nil;
|
||||
GNSWeavePacket *packet = [GNSWeavePacket parseData:serializedPacket error:&error];
|
||||
XCTAssertNotNil(packet);
|
||||
XCTAssertTrue([packet isKindOfClass:[GNSWeaveConnectionRequestPacket class]]);
|
||||
XCTAssertEqual(packet.packetCounter, 0);
|
||||
GNSWeaveConnectionRequestPacket *connectionRequestPacket =
|
||||
(GNSWeaveConnectionRequestPacket *)packet;
|
||||
XCTAssertEqual(connectionRequestPacket.minVersion, minVersion);
|
||||
XCTAssertEqual(connectionRequestPacket.maxVersion, maxVersion);
|
||||
XCTAssertEqual(connectionRequestPacket.maxPacketSize, maxPacketSize);
|
||||
XCTAssertEqualObjects(connectionRequestPacket.data, smallPayload);
|
||||
}
|
||||
|
||||
- (void)testParseConnectionConfirmControlPacket {
|
||||
// 1000 0001, control packets have the MSB set and, connection request code is 1. The packet
|
||||
// counter is 0.
|
||||
UInt8 header = (1 << 7) + 1;
|
||||
NSMutableData *payload = [[NSMutableData alloc] init];
|
||||
UInt16 version = 2;
|
||||
UInt16 packetSize = 30;
|
||||
|
||||
// The Weave protocol uses big-endian format for the multi-bytes types.
|
||||
UInt16 versionBigEndian = CFSwapInt16HostToBig(version);
|
||||
UInt16 packetSizeBigEndian = CFSwapInt16HostToBig(packetSize);
|
||||
[payload appendBytes:&versionBigEndian length:sizeof(versionBigEndian)];
|
||||
[payload appendBytes:&packetSizeBigEndian length:sizeof(packetSizeBigEndian)];
|
||||
NSError *error = nil;
|
||||
GNSWeavePacket *packet =
|
||||
[GNSWeavePacket parseData:[self weavePacketWithHeader:header data:payload] error:&error];
|
||||
XCTAssertNotNil(packet);
|
||||
XCTAssertTrue([packet isKindOfClass:[GNSWeaveConnectionConfirmPacket class]]);
|
||||
XCTAssertEqual(packet.packetCounter, 0);
|
||||
GNSWeaveConnectionConfirmPacket *connectionRequestPacket =
|
||||
(GNSWeaveConnectionConfirmPacket *)packet;
|
||||
XCTAssertEqual(connectionRequestPacket.version, version);
|
||||
XCTAssertEqual(connectionRequestPacket.packetSize, packetSize);
|
||||
XCTAssertNil(connectionRequestPacket.data);
|
||||
}
|
||||
|
||||
- (void)testBuildConnectionConfirmControlPacket {
|
||||
UInt16 version = 30;
|
||||
UInt16 packetSize = 21;
|
||||
NSData *smallPayload = [@"Payload" dataUsingEncoding:NSUTF8StringEncoding];
|
||||
GNSWeaveConnectionConfirmPacket *confirmPacket =
|
||||
[[GNSWeaveConnectionConfirmPacket alloc] initWithVersion:version
|
||||
packetSize:packetSize
|
||||
data:smallPayload];
|
||||
NSData *serializedPacket = [confirmPacket serialize];
|
||||
UInt8 firstByte = *(UInt8 *)serializedPacket.bytes;
|
||||
XCTAssertEqual(firstByte, (1 << 7) + 1); // 1000 0001
|
||||
|
||||
NSError *error = nil;
|
||||
GNSWeavePacket *packet = [GNSWeavePacket parseData:serializedPacket error:&error];
|
||||
XCTAssertNotNil(packet);
|
||||
XCTAssertTrue([packet isKindOfClass:[GNSWeaveConnectionConfirmPacket class]]);
|
||||
XCTAssertEqual(packet.packetCounter, 0);
|
||||
GNSWeaveConnectionConfirmPacket *connectionConfirmPacket =
|
||||
(GNSWeaveConnectionConfirmPacket *)packet;
|
||||
XCTAssertEqual(connectionConfirmPacket.version, version);
|
||||
XCTAssertEqual(connectionConfirmPacket.packetSize, packetSize);
|
||||
XCTAssertEqualObjects(connectionConfirmPacket.data, smallPayload);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "internal/platform/cancellation_flag.h"
|
||||
#include "internal/platform/implementation/ble_v2.h"
|
||||
#include "internal/platform/implementation/bluetooth_adapter.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/GNCMConnection.h"
|
||||
#include "internal/platform/implementation/ios/bluetooth_adapter.h"
|
||||
|
||||
@class GNCMBlePeripheral, GNCMBleCentral;
|
||||
@@ -32,29 +33,107 @@ namespace location {
|
||||
namespace nearby {
|
||||
namespace ios {
|
||||
|
||||
/** InputStream that reads from GNCMConnection. */
|
||||
class BleInputStream : public InputStream {
|
||||
public:
|
||||
BleInputStream();
|
||||
~BleInputStream() override;
|
||||
|
||||
ExceptionOr<ByteArray> Read(std::int64_t size) override;
|
||||
Exception Close() override;
|
||||
|
||||
GNCMConnectionHandlers *GetConnectionHandlers() { return connectionHandlers_; }
|
||||
|
||||
private:
|
||||
GNCMConnectionHandlers *connectionHandlers_;
|
||||
NSMutableArray<NSData *> *newDataPackets_;
|
||||
NSMutableData *accumulatedData_;
|
||||
NSCondition *condition_;
|
||||
};
|
||||
|
||||
/** OutputStream that writes to GNCMConnection. */
|
||||
class BleOutputStream : public OutputStream {
|
||||
public:
|
||||
explicit BleOutputStream(id<GNCMConnection> connection)
|
||||
: connection_(connection), condition_([[NSCondition alloc] init]) {}
|
||||
~BleOutputStream() override;
|
||||
|
||||
Exception Write(const ByteArray &data) override;
|
||||
Exception Flush() override;
|
||||
Exception Close() override;
|
||||
|
||||
private:
|
||||
id<GNCMConnection> connection_;
|
||||
NSCondition *condition_;
|
||||
};
|
||||
|
||||
/** Concrete BleSocket implementation. */
|
||||
class BleSocket : public api::ble_v2::BleSocket {
|
||||
public:
|
||||
BleSocket() = default;
|
||||
explicit BleSocket(id<GNCMConnection> connection);
|
||||
~BleSocket() override;
|
||||
|
||||
InputStream &GetInputStream() override { return *input_stream_; }
|
||||
OutputStream &GetOutputStream() override { return *output_stream_; }
|
||||
Exception Close() override ABSL_LOCKS_EXCLUDED(mutex_);
|
||||
BlePeripheral *GetRemotePeripheral() override { return nullptr; }
|
||||
|
||||
bool IsClosed() const ABSL_LOCKS_EXCLUDED(mutex_);
|
||||
|
||||
private:
|
||||
void DoClose() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
|
||||
|
||||
mutable absl::Mutex mutex_;
|
||||
bool closed_ ABSL_GUARDED_BY(mutex_) = false;
|
||||
std::unique_ptr<BleInputStream> input_stream_;
|
||||
std::unique_ptr<BleOutputStream> output_stream_;
|
||||
};
|
||||
|
||||
/** Concrete BleServerSocket implementation. */
|
||||
class BleServerSocket : public api::ble_v2::BleServerSocket {
|
||||
public:
|
||||
~BleServerSocket() override;
|
||||
|
||||
std::unique_ptr<api::ble_v2::BleSocket> Accept() override ABSL_LOCKS_EXCLUDED(mutex_);
|
||||
Exception Close() override ABSL_LOCKS_EXCLUDED(mutex_);
|
||||
|
||||
bool Connect(std::unique_ptr<BleSocket> socket) ABSL_LOCKS_EXCLUDED(mutex_);
|
||||
void SetCloseNotifier(std::function<void()> notifier) ABSL_LOCKS_EXCLUDED(mutex_);
|
||||
|
||||
private:
|
||||
Exception DoClose() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
|
||||
|
||||
mutable absl::Mutex mutex_;
|
||||
absl::CondVar cond_;
|
||||
absl::flat_hash_set<std::unique_ptr<BleSocket>> pending_sockets_ ABSL_GUARDED_BY(mutex_);
|
||||
std::function<void()> close_notifier_ ABSL_GUARDED_BY(mutex_);
|
||||
bool closed_ ABSL_GUARDED_BY(mutex_) = false;
|
||||
};
|
||||
|
||||
/** Concrete BleMedium implementation. */
|
||||
class BleMedium : public api::ble_v2::BleMedium {
|
||||
public:
|
||||
explicit BleMedium(api::BluetoothAdapter& adapter);
|
||||
explicit BleMedium(api::BluetoothAdapter &adapter);
|
||||
|
||||
// api::BleMedium:
|
||||
bool StartAdvertising(const api::ble_v2::BleAdvertisementData& advertising_data,
|
||||
bool StartAdvertising(const api::ble_v2::BleAdvertisementData &advertising_data,
|
||||
api::ble_v2::AdvertiseParameters advertise_set_parameters) override;
|
||||
bool StopAdvertising() override;
|
||||
bool StartScanning(const Uuid& service_uuid, api::ble_v2::TxPowerLevel tx_power_level,
|
||||
bool StartScanning(const Uuid &service_uuid, api::ble_v2::TxPowerLevel tx_power_level,
|
||||
api::ble_v2::BleMedium::ScanCallback scan_callback) override;
|
||||
bool StopScanning() override;
|
||||
std::unique_ptr<api::ble_v2::GattServer> StartGattServer(
|
||||
api::ble_v2::ServerGattConnectionCallback callback) override;
|
||||
std::unique_ptr<api::ble_v2::GattClient> ConnectToGattServer(
|
||||
api::ble_v2::BlePeripheral& peripheral, api::ble_v2::TxPowerLevel tx_power_level,
|
||||
api::ble_v2::BlePeripheral &peripheral, api::ble_v2::TxPowerLevel tx_power_level,
|
||||
api::ble_v2::ClientGattConnectionCallback callback) override;
|
||||
std::unique_ptr<api::ble_v2::BleServerSocket> OpenServerSocket(
|
||||
const std::string& service_id) override;
|
||||
std::unique_ptr<api::ble_v2::BleSocket> Connect(const std::string& service_id,
|
||||
const std::string &service_id) override;
|
||||
std::unique_ptr<api::ble_v2::BleSocket> Connect(const std::string &service_id,
|
||||
api::ble_v2::TxPowerLevel tx_power_level,
|
||||
api::ble_v2::BlePeripheral& peripheral,
|
||||
CancellationFlag* cancellation_flag) override;
|
||||
api::ble_v2::BlePeripheral &peripheral,
|
||||
CancellationFlag *cancellation_flag) override;
|
||||
bool IsExtendedAdvertisementsAvailable() override;
|
||||
|
||||
private:
|
||||
@@ -62,53 +141,57 @@ class BleMedium : public api::ble_v2::BleMedium {
|
||||
class GattServer : public api::ble_v2::GattServer {
|
||||
public:
|
||||
GattServer() = default;
|
||||
explicit GattServer(GNCMBlePeripheral* peripheral) : peripheral_(peripheral) {}
|
||||
explicit GattServer(GNCMBlePeripheral *peripheral) : peripheral_(peripheral) {}
|
||||
|
||||
absl::optional<api::ble_v2::GattCharacteristic> CreateCharacteristic(
|
||||
const Uuid& service_uuid, const Uuid& characteristic_uuid,
|
||||
const std::vector<api::ble_v2::GattCharacteristic::Permission>& permissions,
|
||||
const std::vector<api::ble_v2::GattCharacteristic::Property>& properties) override;
|
||||
const Uuid &service_uuid, const Uuid &characteristic_uuid,
|
||||
const std::vector<api::ble_v2::GattCharacteristic::Permission> &permissions,
|
||||
const std::vector<api::ble_v2::GattCharacteristic::Property> &properties) override;
|
||||
|
||||
bool UpdateCharacteristic(const api::ble_v2::GattCharacteristic& characteristic,
|
||||
const location::nearby::ByteArray& value) override;
|
||||
bool UpdateCharacteristic(const api::ble_v2::GattCharacteristic &characteristic,
|
||||
const location::nearby::ByteArray &value) override;
|
||||
void Stop() override;
|
||||
|
||||
private:
|
||||
GNCMBlePeripheral* peripheral_;
|
||||
GNCMBlePeripheral *peripheral_;
|
||||
};
|
||||
|
||||
// A concrete implemenation for GattClient.
|
||||
class GattClient : public api::ble_v2::GattClient {
|
||||
public:
|
||||
GattClient() = default;
|
||||
explicit GattClient(GNCMBleCentral* central, const std::string& peripheral_id)
|
||||
explicit GattClient(GNCMBleCentral *central, const std::string &peripheral_id)
|
||||
: central_(central), peripheral_id_(peripheral_id) {}
|
||||
|
||||
bool DiscoverServiceAndCharacteristics(const Uuid& service_uuid,
|
||||
const std::vector<Uuid>& characteristic_uuids) override;
|
||||
bool DiscoverServiceAndCharacteristics(const Uuid &service_uuid,
|
||||
const std::vector<Uuid> &characteristic_uuids) override;
|
||||
|
||||
// NOLINTNEXTLINE
|
||||
absl::optional<api::ble_v2::GattCharacteristic> GetCharacteristic(
|
||||
const Uuid& service_uuid, const Uuid& characteristic_uuid) override;
|
||||
const Uuid &service_uuid, const Uuid &characteristic_uuid) override;
|
||||
|
||||
// NOLINTNEXTLINE
|
||||
absl::optional<ByteArray> ReadCharacteristic(
|
||||
const api::ble_v2::GattCharacteristic& characteristic) override;
|
||||
const api::ble_v2::GattCharacteristic &characteristic) override;
|
||||
|
||||
bool WriteCharacteristic(const api::ble_v2::GattCharacteristic& characteristic,
|
||||
const ByteArray& value) override;
|
||||
bool WriteCharacteristic(const api::ble_v2::GattCharacteristic &characteristic,
|
||||
const ByteArray &value) override;
|
||||
|
||||
void Disconnect() override;
|
||||
|
||||
private:
|
||||
GNCMBleCentral* central_;
|
||||
GNCMBleCentral *central_;
|
||||
std::string peripheral_id_;
|
||||
absl::flat_hash_map<api::ble_v2::GattCharacteristic, ByteArray> gatt_characteristic_values_;
|
||||
};
|
||||
|
||||
BluetoothAdapter* adapter_;
|
||||
GNCMBlePeripheral* peripheral_;
|
||||
GNCMBleCentral* central_;
|
||||
absl::Mutex mutex_;
|
||||
BluetoothAdapter *adapter_;
|
||||
GNCMBlePeripheral *peripheral_;
|
||||
GNCMBleCentral *central_;
|
||||
absl::flat_hash_map<std::string, BleServerSocket *> server_sockets_ ABSL_GUARDED_BY(mutex_);
|
||||
absl::flat_hash_map<std::string, GNCMConnectionRequester> connection_requesters_
|
||||
ABSL_GUARDED_BY(mutex_);
|
||||
};
|
||||
|
||||
} // namespace ios
|
||||
|
||||
@@ -15,13 +15,16 @@
|
||||
#import "internal/platform/implementation/ios/ble.h"
|
||||
|
||||
#include <CoreBluetooth/CoreBluetooth.h>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "internal/platform/implementation/ble_v2.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/GNCMBleCentral.h"
|
||||
#import "internal/platform/implementation/ios/Mediums/Ble/GNCMBlePeripheral.h"
|
||||
#include "internal/platform/implementation/ios/bluetooth_adapter.h"
|
||||
#include "internal/platform/implementation/ios/utils.h"
|
||||
#import "GoogleToolboxForMac/GTMLogger.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
@@ -76,6 +79,239 @@ using ::location::nearby::api::ble_v2::BleAdvertisementData;
|
||||
using ::location::nearby::api::ble_v2::TxPowerLevel;
|
||||
using ScanCallback = ::location::nearby::api::ble_v2::BleMedium::ScanCallback;
|
||||
|
||||
/** InputStream that reads from GNCMConnection. */
|
||||
BleInputStream::BleInputStream()
|
||||
: newDataPackets_([NSMutableArray array]),
|
||||
accumulatedData_([NSMutableData data]),
|
||||
condition_([[NSCondition alloc] init]) {
|
||||
// Create the handlers of incoming data from the remote endpoint.
|
||||
connectionHandlers_ = [GNCMConnectionHandlers
|
||||
payloadHandler:^(NSData* data) {
|
||||
[condition_ lock];
|
||||
// Add the incoming data to the data packet array to be processed in read() below.
|
||||
[newDataPackets_ addObject:data];
|
||||
[condition_ signal];
|
||||
[condition_ unlock];
|
||||
}
|
||||
disconnectedHandler:^{
|
||||
[condition_ lock];
|
||||
// Release the data packet array, meaning the stream has been closed or severed.
|
||||
newDataPackets_ = nil;
|
||||
[condition_ signal];
|
||||
[condition_ unlock];
|
||||
}];
|
||||
}
|
||||
|
||||
BleInputStream::~BleInputStream() {
|
||||
NSCAssert(!newDataPackets_, @"BleInputStream not closed before destruction");
|
||||
}
|
||||
|
||||
ExceptionOr<ByteArray> BleInputStream::Read(std::int64_t size) {
|
||||
// Block until either (a) the connection has been closed, (b) we have enough data to return.
|
||||
NSData* dataToReturn;
|
||||
[condition_ lock];
|
||||
while (true) {
|
||||
// Check if the stream has been closed or severed.
|
||||
if (!newDataPackets_) break;
|
||||
|
||||
if (newDataPackets_.count > 0) {
|
||||
// Add the packet data to the accumulated data.
|
||||
for (NSData* data in newDataPackets_) {
|
||||
if (data.length > 0) {
|
||||
[accumulatedData_ appendData:data];
|
||||
}
|
||||
}
|
||||
[newDataPackets_ removeAllObjects];
|
||||
}
|
||||
|
||||
if ((size == -1) && (accumulatedData_.length > 0)) {
|
||||
// Return all of the data.
|
||||
dataToReturn = accumulatedData_;
|
||||
accumulatedData_ = [NSMutableData data];
|
||||
break;
|
||||
} else if (accumulatedData_.length > 0) {
|
||||
// Return up to |size| bytes of the data.
|
||||
std::int64_t sizeToReturn = (accumulatedData_.length < size) ? accumulatedData_.length : size;
|
||||
NSRange range = NSMakeRange(0, (NSUInteger)sizeToReturn);
|
||||
dataToReturn = [accumulatedData_ subdataWithRange:range];
|
||||
[accumulatedData_ replaceBytesInRange:range withBytes:nil length:0];
|
||||
break;
|
||||
}
|
||||
|
||||
[condition_ wait];
|
||||
}
|
||||
[condition_ unlock];
|
||||
|
||||
if (dataToReturn) {
|
||||
GTMLoggerInfo(@"[NEARBY] Input stream: Received data of size: %lu",
|
||||
(unsigned long)dataToReturn.length);
|
||||
return ExceptionOr<ByteArray>(ByteArrayFromNSData(dataToReturn));
|
||||
} else {
|
||||
return ExceptionOr<ByteArray>{Exception::kIo};
|
||||
}
|
||||
}
|
||||
|
||||
Exception BleInputStream::Close() {
|
||||
// Unblock pending read operation.
|
||||
[condition_ lock];
|
||||
newDataPackets_ = nil;
|
||||
[condition_ signal];
|
||||
[condition_ unlock];
|
||||
return {Exception::kSuccess};
|
||||
}
|
||||
|
||||
/** OutputStream that writes to GNCMConnection. */
|
||||
BleOutputStream::~BleOutputStream() {
|
||||
NSCAssert(!connection_, @"BleOutputStream not closed before destruction");
|
||||
}
|
||||
|
||||
Exception BleOutputStream::Write(const ByteArray& data) {
|
||||
[condition_ lock];
|
||||
GTMLoggerInfo(@"[NEARBY] Sending data of size: %lu",
|
||||
(unsigned long)NSDataFromByteArray(data).length);
|
||||
|
||||
NSMutableData* packet = [NSMutableData dataWithData:NSDataFromByteArray(data)];
|
||||
|
||||
// Send the data, blocking until the completion handler is called.
|
||||
__block GNCMPayloadResult sendResult = GNCMPayloadResultFailure;
|
||||
__block bool isComplete = NO;
|
||||
NSCondition* condition = condition_; // don't capture |this| in completion
|
||||
|
||||
// Check if connection_ is nil, then just don't wait and return as failure.
|
||||
if (connection_ != nil) {
|
||||
[connection_ sendData:packet
|
||||
progressHandler:^(size_t count) {
|
||||
}
|
||||
completion:^(GNCMPayloadResult result) {
|
||||
// Make sure we haven't already reported completion before. This prevents a crash
|
||||
// where we try leaving a dispatch group more times than we entered it.
|
||||
// b/79095653.
|
||||
if (isComplete) {
|
||||
return;
|
||||
}
|
||||
isComplete = YES;
|
||||
sendResult = result;
|
||||
[condition lock];
|
||||
[condition signal];
|
||||
[condition unlock];
|
||||
}];
|
||||
[condition_ wait];
|
||||
[condition_ unlock];
|
||||
} else {
|
||||
sendResult = GNCMPayloadResultFailure;
|
||||
[condition_ unlock];
|
||||
}
|
||||
|
||||
if (sendResult == GNCMPayloadResultSuccess) {
|
||||
return {Exception::kSuccess};
|
||||
} else {
|
||||
return {Exception::kIo};
|
||||
}
|
||||
}
|
||||
|
||||
Exception BleOutputStream::Flush() {
|
||||
// The write() function blocks until the data is received by the remote endpoint, so there's
|
||||
// nothing to do here.
|
||||
return {Exception::kSuccess};
|
||||
}
|
||||
|
||||
Exception BleOutputStream::Close() {
|
||||
// Unblock pending write operation.
|
||||
[condition_ lock];
|
||||
connection_ = nil;
|
||||
[condition_ signal];
|
||||
[condition_ unlock];
|
||||
return {Exception::kSuccess};
|
||||
}
|
||||
|
||||
/** BleSocket implementation.*/
|
||||
BleSocket::BleSocket(id<GNCMConnection> connection)
|
||||
: input_stream_(new BleInputStream()), output_stream_(new BleOutputStream(connection)) {}
|
||||
|
||||
BleSocket::~BleSocket() {
|
||||
absl::MutexLock lock(&mutex_);
|
||||
DoClose();
|
||||
}
|
||||
|
||||
bool BleSocket::IsClosed() const {
|
||||
absl::MutexLock lock(&mutex_);
|
||||
return closed_;
|
||||
}
|
||||
|
||||
Exception BleSocket::Close() {
|
||||
absl::MutexLock lock(&mutex_);
|
||||
DoClose();
|
||||
return {Exception::kSuccess};
|
||||
}
|
||||
|
||||
void BleSocket::DoClose() {
|
||||
if (!closed_) {
|
||||
input_stream_->Close();
|
||||
output_stream_->Close();
|
||||
closed_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
/** WifiLanServerSocket implementation. */
|
||||
BleServerSocket::~BleServerSocket() {
|
||||
absl::MutexLock lock(&mutex_);
|
||||
DoClose();
|
||||
}
|
||||
|
||||
std::unique_ptr<api::ble_v2::BleSocket> BleServerSocket::Accept() {
|
||||
absl::MutexLock lock(&mutex_);
|
||||
while (!closed_ && pending_sockets_.empty()) {
|
||||
cond_.Wait(&mutex_);
|
||||
}
|
||||
// Return early if closed.
|
||||
if (closed_) return {};
|
||||
|
||||
auto remote_socket = std::move(pending_sockets_.extract(pending_sockets_.begin()).value());
|
||||
return std::move(remote_socket);
|
||||
}
|
||||
|
||||
bool BleServerSocket::Connect(std::unique_ptr<BleSocket> socket) {
|
||||
absl::MutexLock lock(&mutex_);
|
||||
if (closed_) {
|
||||
return false;
|
||||
}
|
||||
// add client socket to the pending list
|
||||
pending_sockets_.insert(std::move(socket));
|
||||
cond_.SignalAll();
|
||||
if (closed_) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void BleServerSocket::SetCloseNotifier(std::function<void()> notifier) {
|
||||
absl::MutexLock lock(&mutex_);
|
||||
close_notifier_ = std::move(notifier);
|
||||
}
|
||||
|
||||
Exception BleServerSocket::Close() {
|
||||
absl::MutexLock lock(&mutex_);
|
||||
return DoClose();
|
||||
}
|
||||
|
||||
Exception BleServerSocket::DoClose() {
|
||||
bool should_notify = !closed_;
|
||||
closed_ = true;
|
||||
if (should_notify) {
|
||||
cond_.SignalAll();
|
||||
if (close_notifier_) {
|
||||
auto notifier = std::move(close_notifier_);
|
||||
mutex_.Unlock();
|
||||
// Notifier may contain calls to public API, and may cause deadlock, if
|
||||
// mutex_ is held during the call.
|
||||
notifier();
|
||||
mutex_.Lock();
|
||||
}
|
||||
}
|
||||
return {Exception::kSuccess};
|
||||
}
|
||||
|
||||
/** BleMedium implementation. */
|
||||
BleMedium::BleMedium(::location::nearby::api::BluetoothAdapter& adapter)
|
||||
: adapter_(static_cast<BluetoothAdapter*>(&adapter)) {}
|
||||
|
||||
@@ -156,14 +392,56 @@ std::unique_ptr<api::ble_v2::GattClient> BleMedium::ConnectToGattServer(
|
||||
|
||||
std::unique_ptr<api::ble_v2::BleServerSocket> BleMedium::OpenServerSocket(
|
||||
const std::string& service_id) {
|
||||
return nullptr;
|
||||
auto server_socket = std::make_unique<BleServerSocket>();
|
||||
server_socket->SetCloseNotifier([this, service_id]() {
|
||||
absl::MutexLock lock(&mutex_);
|
||||
server_sockets_.erase(service_id);
|
||||
});
|
||||
absl::MutexLock lock(&mutex_);
|
||||
server_sockets_.insert({service_id, server_socket.get()});
|
||||
return server_socket;
|
||||
}
|
||||
|
||||
std::unique_ptr<api::ble_v2::BleSocket> BleMedium::Connect(const std::string& service_id,
|
||||
TxPowerLevel tx_power_level,
|
||||
api::ble_v2::BlePeripheral& peripheral,
|
||||
CancellationFlag* cancellation_flag) {
|
||||
return nullptr;
|
||||
GNCMConnectionRequester connection_requester = nil;
|
||||
{
|
||||
absl::MutexLock lock(&mutex_);
|
||||
const auto& it = connection_requesters_.find(service_id);
|
||||
if (it == connection_requesters_.end()) {
|
||||
return {};
|
||||
}
|
||||
connection_requester = it->second;
|
||||
}
|
||||
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
dispatch_group_enter(group);
|
||||
__block std::unique_ptr<BleSocket> socket;
|
||||
if (connection_requester != nil) {
|
||||
if (cancellation_flag->Cancelled()) {
|
||||
GTMLoggerError(@"[NEARBY] BLE Connect: Has been cancelled: service_id=%@",
|
||||
ObjCStringFromCppString(service_id));
|
||||
dispatch_group_leave(group); // unblock
|
||||
return {};
|
||||
}
|
||||
|
||||
connection_requester(^(id<GNCMConnection> connection) {
|
||||
// If the connection wasn't successfully established, return a NULL socket.
|
||||
if (connection) {
|
||||
socket = std::make_unique<BleSocket>(connection);
|
||||
}
|
||||
|
||||
dispatch_group_leave(group); // unblock
|
||||
return socket != nullptr
|
||||
? static_cast<BleInputStream&>(socket->GetInputStream()).GetConnectionHandlers()
|
||||
: nullptr;
|
||||
});
|
||||
}
|
||||
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
|
||||
|
||||
return std::move(socket);
|
||||
}
|
||||
|
||||
bool BleMedium::IsExtendedAdvertisementsAvailable() { return false; }
|
||||
|
||||
Reference in New Issue
Block a user