Фоновое сканирование BLE iOS

#ios #background #bluetooth-lowenergy #scanning

#iOS #фон #bluetooth-низкое энергопотребление #сканирование

Вопрос:

Я начинаю разрабатывать приложение, которое может подключаться к периферийным устройствам. Одна из проблем, с которой я сталкиваюсь сейчас, — это фоновое сканирование bluetooth. Когда я проверил сайт разработчиков Apple, они дают некоторые инструкции, как работать в фоновом режиме, но я не уверен, куда я могу поместить эти коды в свой проект.

Приложение должно получать всплывающее уведомление, когда периферийные устройства дают команду. На данный момент оно работает на переднем плане.

я поместил коды фонового сканирования в AppDelegate

 //
//  AppDelegate.m
//  WITLock
//
//  Created by NTekSystem on 6/21/16.
//  Copyright © 2016 Ntek. All rights reserved.
//

#import "AppDelegate.h"
#import "ControlViewController.h"
#import <CoreBluetooth/CoreBluetooth.h>
#import "TransferService.h"
//#import "DeviceListViewController.h"
#import <AVFoundation/AVAudioPlayer.h>

@interface AppDelegate ()<CBCentralManagerDelegate, CBPeripheralDelegate>
@property (strong, nonatomic) CBCentralManager      *centralManager;
//@property (strong, nonatomic) CBPeripheral          *discoveredPeripheral;
@property (strong, nonatomic) NSMutableData         *data;
@property (strong, nonatomic) AVAudioPlayer *audioPlayer;


@end

@implementation AppDelegate
{
    #define UIColorFromRGB(rgbValue) [UIColor colorWithRed:((float)((rgbValue amp; 0xFF0000) >> 16))/255.0 green:((float)((rgbValue amp; 0xFF00) >> 8))/255.0 blue:((float)(rgbValue amp; 0xFF))/255.0 alpha:1.0]

}

NSString *characteristicVal;

NSString *tenchar;
NSString *hexAppDel = @"0";
NSUInteger hexAsIntAppDel;

//Binary to String Conversion
-(NSString *)toBinary:(NSUInteger)input
{
    NSString *binaryString;
    if (input == 1 || input == 0)
    {
        return [NSString stringWithFormat:@"%lu", (unsigned long)input];
    }
    else
    {

        binaryString = [NSString stringWithFormat:@"%@%lu", [self toBinary:input / 2], input % 2];
    }
    return binaryString;
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {


    //[self scan];


    [[UINavigationBar appearance] setBarTintColor:UIColorFromRGB(0xFE5E00)]; //navigation bar
    //[[UINavigationBar appearance] setBackgroundImage:[UIImage imageNamed:@"bg_teal_gradient.png"] forBarMetrics:UIBarMetricsDefault];

    NSShadow *shadow = [[NSShadow alloc] init];
    shadow.shadowColor = [UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.8];
    shadow.shadowOffset = CGSizeMake(0, 1);
    [[UINavigationBar appearance] setTitleTextAttributes: [NSDictionary dictionaryWithObjectsAndKeys:
                                                           [UIColor colorWithRed:245.0/255.0 green:245.0/255.0 blue:245.0/255.0 alpha:1.0], NSForegroundColorAttributeName,
                                                           shadow, NSShadowAttributeName,
                                                           [UIFont fontWithName:@"HelveticaNeue-CondensedBlack" size:18.0], NSFontAttributeName, nil]];
    [[UINavigationBar appearance] setTintColor:[UIColor whiteColor]];



    UITabBarController *tabBar = (UITabBarController *)self.window.rootViewController;
    [tabBar setDelegate:self];

     //Slide Menu
    ControlViewController *frontViewController = [[ControlViewController alloc] init];

    UINavigationController *frontNavigationController = [[UINavigationController alloc] initWithRootViewController:frontViewController];

    //_centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil];
    _centralManager =
    [[CBCentralManager alloc] initWithDelegate:self queue:nil
                                       options:@{ CBCentralManagerOptionRestoreIdentifierKey:
                                                      @"myCentralManagerIdentifier" }];

    NSArray *centralManagerIdentifiers = launchOptions[UIApplicationLaunchOptionsBluetoothCentralsKey];



    // And somewhere to store the incoming data
    _data = [[NSMutableData alloc] init];

       // Override point for customization after application launch.

    return YES;

}

- (void)centralManager:(CBPeripheralManager *)central
         willRestoreState:(NSDictionary *)dict
{

    NSArray *services = dict[CBCentralManagerRestoredStateScanServicesKey];
    NSLog(@"NSLog services %@ ", services);
    kill(getpid(), SIGKILL);

}

#pragma mark - Central Methods

/** centralManagerDidUpdateState is a required protocol method.
 *  Usually, you'd check for other states to make sure the current device supports LE, is powered on, etc.
 *  In this instance, we're just using it to wait for CBCentralManagerStatePoweredOn, which indicates
 *  the Central is ready to be used.
 */
- (void)centralManagerDidUpdateState:(CBCentralManager *)central
{
    if (central.state != CBCentralManagerStatePoweredOn)
    {
        // In a real app, you'd deal with all the states correctly
        return;
    }

    // The state must be CBCentralManagerStatePoweredOn...



    // ... so start scanning
    [self scan];

}


/** Scan for peripherals - specifically for our service's 128bit CBUUID
 */
- (void)scan

{
//    CBCentralManagerOptionRestoreIdentifierKey:@"YourUniqueIdentifier"}

    NSArray *service_uuid = @[[CBUUID UUIDWithString:TRANSFER_SERVICE_UUID]];

//    [self.centralManager scanForPeripheralsWithServices:@[[CBUUID UUIDWithString:TRANSFER_SERVICE_UUID]] options:@{CBCentralManagerScanOptionAllowDuplicatesKey:@(YES)}];


    [self.centralManager scanForPeripheralsWithServices:service_uuid options:nil];

    NSLog(@"Scanning started");
}


/** This callback comes whenever a peripheral that is advertising the TRANSFER_SERVICE_UUID is discovered.
 *  We check the RSSI, to make sure it's close enough that we're interested in it, and if it is,
 *  we start the connection process
 */

- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI
{
    //    // Reject any where the value is above reasonable range
    //    if (RSSI.integerValue > -15) {
    //        return;
    //    }
    //
    //    // Reject if the signal strength is too low to be close enough (Close is around -22dB)
    if (RSSI.integerValue < -68) {
        return;
    }

    //getting all advertisment data//
    NSLog(@"Advertisement Data %@", advertisementData);

    //getting service data//
    NSData *serviceData = [advertisementData objectForKey:CBAdvertisementDataServiceDataKey];
    NSLog(@"Service Data %@ ",serviceData);

    //nsdata convert to string //
    NSString* serviceDataStr = [NSString stringWithFormat:@"%@",serviceData];
    NSLog(@"Service String %@ ",serviceDataStr);

    //remove all spaces//
    NSString *serviceDataNoSpace = [ serviceDataStr stringByReplacingOccurrencesOfString:@" " withString:@""];
    NSLog(@"No space %@", serviceDataNoSpace);

    NSString *pername =[advertisementData objectForKey:CBAdvertisementDataLocalNameKey];
    NSLog(@"Pername %@ ", pername);

    if([pername isEqualToString:@"WL-00000003"])
    {

    if (serviceDataNoSpace.length != 0 amp;amp; ![serviceDataNoSpace isEqualToString:@"(null)"] )
    {
        NSLog(@"Length Characteristic Value %lu", (unsigned long)[serviceDataNoSpace length]);

        int indexten = 10;


        tenchar = [NSString stringWithFormat:@"%c", [serviceDataNoSpace characterAtIndex:indexten-1]];

        [[NSScanner scannerWithString:tenchar] scanHexInt:amp;hexAsIntAppDel];
        [[NSScanner scannerWithString:tenchar] scanHexInt:amp;hexAsIntAppDel];

        NSString *binary = [NSString stringWithFormat:@"%@", [self toBinary:hexAsIntAppDel]];


        if (binary.length == 3)
        {
            binary = [@"0" stringByAppendingString:binary];
        }
        else if (binary.length == 2)
        {
            binary = [@"00" stringByAppendingString:binary];
        }
        else if (binary.length == 1)
        {
            binary = [@"000" stringByAppendingString:binary];
        }
        NSLog(@"Binary %@", binary);



        NSLog(@"Value of Tenth Character %@", tenchar);


        NSString *Port1 = [NSString stringWithFormat:@"%c", [binary characterAtIndex:0]];
        NSString *Port2 = [NSString stringWithFormat:@"%c", [binary characterAtIndex:1]];
        NSString *Port3 = [NSString stringWithFormat:@"%c", [binary characterAtIndex:2]];
        NSString *Port4 = [NSString stringWithFormat:@"%c", [binary characterAtIndex:3]];
        NSLog(@"Log ng mga Port %@ , %@ ,%@ ,%@ ", Port1, Port2,Port3, Port4);


        if ([Port1 isEqualToString:@"0"])
        {
            NSLog(@"Inactive Background");
        }
        else if ([Port1 isEqualToString:@"1"])
        {
            NSURL *url = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/pipipipick.mp3", [[NSBundle mainBundle] resourcePath]]];

            NSError *error;

            _audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:amp;error];
            _audioPlayer.numberOfLoops = -1;

            NSLog(@"Active Background");
            UIAlertController *alertController = [UIAlertController
                                                              alertControllerWithTitle:@"Pipipipick"
                                                              message:@""
                                                              preferredStyle:UIAlertControllerStyleAlert];


            UIAlertAction *cancelAction = [UIAlertAction
                                                       actionWithTitle:NSLocalizedString(@"Cancel", @"OK action")
                                                       style:UIAlertActionStyleDefault
                                                       handler:^(UIAlertAction *action)
                                                       {
                                                           [_audioPlayer stop];
                                                       }];

            [alertController addAction:cancelAction];
            [_audioPlayer play];

        [self.window.rootViewController presentViewController:alertController animated:YES completion:nil];


        }


        if ([Port2 isEqualToString:@"0"])
        {
           NSLog(@"Inactive Background");
        }
        else if ([Port2 isEqualToString:@"1"])
        {
            NSURL *url = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/pipipipick.mp3", [[NSBundle mainBundle] resourcePath]]];

            NSError *error;

            _audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:amp;error];
            _audioPlayer.numberOfLoops = -1;

            NSLog(@"Active Background");
            UIAlertController *alertController = [UIAlertController
                                                  alertControllerWithTitle:@"Pipipipick"
                                                  message:@""
                                                  preferredStyle:UIAlertControllerStyleAlert];


            UIAlertAction *cancelAction = [UIAlertAction
                                           actionWithTitle:NSLocalizedString(@"Cancel", @"OK action")
                                           style:UIAlertActionStyleDefault
                                           handler:^(UIAlertAction *action)
                                           {
                                               UITextField *login = alertController.textFields.firstObject;

                                               [_audioPlayer stop];
                                           }];

            [alertController addAction:cancelAction];
            [_audioPlayer play];
            [self.window.rootViewController presentViewController:alertController animated:YES completion:nil];
        }

        if ([Port3 isEqualToString:@"0"])
        {
            NSLog(@"Inactive Background");
        }

        else if ([Port3 isEqualToString:@"1"])
        {
            NSURL *url = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/pipipipick.mp3", [[NSBundle mainBundle] resourcePath]]];

            NSError *error;

            _audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:amp;error];
            _audioPlayer.numberOfLoops = -1;

            NSLog(@"Active Background");
            UIAlertController *alertController = [UIAlertController
                                                  alertControllerWithTitle:@"Pipipipick"
                                                  message:@""
                                                  preferredStyle:UIAlertControllerStyleAlert];


            UIAlertAction *cancelAction = [UIAlertAction
                                           actionWithTitle:NSLocalizedString(@"Cancel", @"OK action")
                                           style:UIAlertActionStyleDefault
                                           handler:^(UIAlertAction *action)
                                           {
                                               UITextField *login = alertController.textFields.firstObject;

                                               [_audioPlayer stop];
                                           }];

            [alertController addAction:cancelAction];
            [_audioPlayer play];
            [self.window.rootViewController presentViewController:alertController animated:YES completion:nil];

        }

        if ([Port4 isEqualToString:@"0"])
        {
            NSLog(@"Inactive Background");
        }
        else if ([Port4 isEqualToString:@"1"])
        {
            NSURL *url = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/pipipipick.mp3", [[NSBundle mainBundle] resourcePath]]];

            NSError *error;

            _audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:amp;error];
            _audioPlayer.numberOfLoops = -1;

            NSLog(@"Active Background");
            UIAlertController *alertController = [UIAlertController
                                                  alertControllerWithTitle:@"Pipipipick"
                                                  message:@""
                                                  preferredStyle:UIAlertControllerStyleAlert];


            UIAlertAction *cancelAction = [UIAlertAction
                                           actionWithTitle:NSLocalizedString(@"Cancel", @"OK action")
                                           style:UIAlertActionStyleDefault
                                           handler:^(UIAlertAction *action)
                                           {
                                               UITextField *login = alertController.textFields.firstObject;

                                               [_audioPlayer stop];
                                           }];

            [alertController addAction:cancelAction];
            [_audioPlayer play];
            [self.window.rootViewController presentViewController:alertController animated:YES completion:nil];

        }

    }
}
    NSLog(@"Discovered %@ at %@", peripheral.name, RSSI);
    NSLog(@"Logging %@ ", peripheral);

    // Ok, it's in range - have we already seen it?
   // if([peripheral.name isEqual:@"SW_TEST_4"])
    //{
        if (self.discoveredPeripheral != peripheral) {

            // Save a local copy of the peripheral, so CoreBluetooth doesn't get rid of it
            self.discoveredPeripheral = peripheral;

            // And connect
            NSLog(@"Connecting to peripheral %@", peripheral);
            //[self.centralManager connectPeripheral:nil options:nil];
        }
    //}

    //<be38fad4 7c0092d2>;
   }


/** If the connection fails for whatever reason, we need to deal with it.
 */
- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error
{
    NSLog(@"Failed to connect to %@. (%@)", peripheral, [error localizedDescription]);
    [self cleanup];
}


/** We've connected to the peripheral, now we need to discover the services and characteristics to find the 'transfer' characteristic.
 */
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
{
    NSLog(@"Peripheral Connected");

    // Stop scanning
    [self.centralManager stopScan];
    NSLog(@"Scanning stopped");

    // Clear the data that we may already have
    [self.data setLength:0];

    // Make sure we get the discovery callbacks
    peripheral.delegate = self;

    // Search only for services that match our UUID
    [peripheral discoverServices:@[[CBUUID UUIDWithString:TRANSFER_SERVICE_UUID]]];
}


/** The Transfer Service was discovered
 */
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error
{
    if (error) {
        NSLog(@"Error discovering services: %@", [error localizedDescription]);
        [self cleanup];
        return;
    }

    // Discover the characteristic we want...

    // Loop through the newly filled peripheral.services array, just in case there's more than one.
    for (CBService *service in peripheral.services)
    {
        [peripheral discoverCharacteristics:@[[CBUUID UUIDWithString:TRANSFER_CHARACTERISTIC_UUID]] forService:service];
        NSLog(@"Discovered service %@", service);

    }
}


/** The Transfer characteristic was discovered.
 *  Once this has been found, we want to subscribe to it, which lets the peripheral know we want the data it contains
 */
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error
{
    // Deal with errors (if any)
    if (error) {
        NSLog(@"Error discovering characteristics: %@", [error localizedDescription]);
        [self cleanup];


        return;
    }

    // Again, we loop through the array, just in case.
    for (CBCharacteristic *characteristic in service.characteristics) {
        [peripheral readValueForCharacteristic:characteristic];
        // And check if it's the right one
        if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:TRANSFER_CHARACTERISTIC_UUID]]) {

            // If it is, subscribe to it
            [peripheral setNotifyValue:YES forCharacteristic:characteristic];
            NSLog(@"Discovered characteristic %@", characteristic);
        }
    }

    // Once this is complete, we just need to wait for the data to come in.
}

/** This callback lets us know more data has arrived via notification on the characteristic
 */


/** The peripheral letting us know whether our subscribe/unsubscribe happened or not
 */
- (void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
{
    if (error) {
        NSLog(@"Error changing notification state: %@", error.localizedDescription);
    }

    // Exit if it's not the transfer characteristic
    if (![characteristic.UUID isEqual:[CBUUID UUIDWithString:TRANSFER_CHARACTERISTIC_UUID]]) {
        NSLog(@"Exit didUpdateNotifficationStateForCharacteristic");
        return;
    }

    // Notification has started
    if (characteristic.isNotifying) {
        NSLog(@"Notification began on %@", characteristic);
    }

    // Notification has stopped
    else {
        // so disconnect from the peripheral
        NSLog(@"Notification stopped on %@.  Disconnecting", characteristic);
        [self.centralManager cancelPeripheralConnection:peripheral];
    }
}


/** Once the disconnection happens, we need to clean up our local copy of the peripheral
 */
- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error
{
    NSLog(@"Peripheral Disconnected");
    self.discoveredPeripheral = nil;

    // We're disconnected, so start scanning again
    [self scan];
}


/** Call this when things either go wrong, or you're done with the connection.
 *  This cancels any subscriptions if there are any, or straight disconnects if not.
 *  (didUpdateNotificationStateForCharacteristic will cancel the connection if a subscription is involved)
 */
- (void)cleanup
{
    NSLog(@"cleanup method");
    // See if we are subscribed to a characteristic on the peripheral
    if (self.discoveredPeripheral.services != nil) {
        for (CBService *service in self.discoveredPeripheral.services) {
            if (service.characteristics != nil) {
                for (CBCharacteristic *characteristic in service.characteristics) {
                    if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:TRANSFER_CHARACTERISTIC_UUID]]) {
                        if (characteristic.isNotifying) {
                            // It is notifying, so unsubscribe
                            NSLog(@"It is notifying, so unsubscribe");
                            [self.discoveredPeripheral setNotifyValue:NO forCharacteristic:characteristic];
                            NSLog(@"Done unsubscribing");
                            // And we're done.
                            return;
                        }
                    }
                }
            }
        }
    }

    // If we've got this far, we're connected, but we're not subscribed, so we just disconnect
    NSLog(@"If we've got this far, we're connected, but we're not subscribed, so we just disconnect");
    [self.centralManager cancelPeripheralConnection:self.discoveredPeripheral];
}




- (void)applicationWillResignActive:(UIApplication *)application {
    // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
    // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
}

- (void)applicationDidEnterBackground:(UIApplication *)application {
    // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
    // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
    NSLog(@"Did Enter in Background");
    //[self scan];
}

- (void)applicationWillEnterForeground:(UIApplication *)application {
    // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
    NSLog(@"Did Enter in Foreground");
    //[self scan];

}


- (void)applicationDidBecomeActive:(UIApplication *)application {
    NSLog(@"Did Enter in Active");
    //[self scan];

    // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}

- (void)applicationWillTerminate:(UIApplication *)application {
    NSLog(@"Did Enter in Terminate");
    //[self scan];

    // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}

@end
  

Ответ №1:

При сканировании устройств на переднем плане вы можете сканировать все, что угодно. В фоновом режиме вы должны указать фактический идентификатор UUID службы, для которого выполняется сканирование. Хорошо, на самом деле это не проблема, так как вы знаете UUID, который ищете. вам необходимо добавить запись UIBackgroundModes в свой Info.plist в разделе возможности проверьте использование аксессуаров bluetooth LE. добавьте свою сканирующую часть кода в метод applicationDidEnterBackground.

Комментарии:

1. Спасибо за ответ, но я установил режим uibackground. Также я установил uuid конкретной службы

2. — (недействительно) applicationDidEnterBackground:(UIApplication *)application { NSLog(@»applicationDidEnterBackground»); NSDictionary *options = [Словарь NSDictionary с объектами и ключами:[NSNumber numberWithBool:ДА], cbcentralmanagers может использовать allowduplicateskey, nil]; [self.centralManager сканирует периферийные устройства с помощью сервисов:@[ SERVICE_UUID,DF_SERVICE1_UUID] параметры:параметры]; }

3. Что означает DF_Service1_uuid, сэр?

4. это один идентификатор пользовательского интерфейса службы.

Ответ №2:

Я боролся с аналогичной проблемой.

Если didDiscoverPeripheral запускается немедленно, когда приложение возвращается из фонового режима, это может быть связано с тем, как периферийное устройство Bluetooth рекламирует себя.

Интервал между рекламными объявлениями должен соответствовать разделу 3.5 Рекомендаций по дизайну аксессуаров Bluetooth для Apple.

По сути, ваше периферийное устройство должно рекламировать каждые 20 миллисекунд в течение как минимум первых 30 секунд, затем продолжайте с интервалами, указанными в документации.

Смотрите ссылку ниже для получения подробной информации и ответа от сотрудников Apple, особенно в отношении ситуации 3).

Ответ на форуме Apple