今是昨非

今是昨非

日出江花红胜火,春来江水绿如蓝

一文学会iOS Bluetooth開発

一文学会 iOS ブルートゥース開発#

背景#

最近、APP とブルートゥースデバイスの接続開発を行っており、ここでは iOS でブルートゥースデバイスに接続する際に注意すべき点を共有します。大まかには以下の内容を含みます:

  • Xcode のブルートゥース権限
  • ブルートゥースデバイスのスキャン方法、Mac アドレスの取得
  • 異なるブルートゥースデバイスの切り替え
  • ブルートゥースコマンドの書き込み
    • データを 16 進数文字列に変換
    • 16 進数を String に変換
    • CRC アルゴリズム
    • データの排他的論理和計算、文字列の排他的論理和
      • 負数の排他的論理和計算
  • 複数のコマンドを順次書き込む

ブルートゥース開発の大まかな流れ#

まず、ブルートゥース開発の流れを理解しましょう。以下のようにまとめられます:

Xcode でブルートゥース権限を設定 -> ブルートゥースを起動 -> 周囲のブルートゥースをスキャン -> 指定のブルートゥースに接続 -> 接続成功を確認 -> ブルートゥースの読み書き -> 接続を切断

フローチャートは以下の通りです:

ブルートゥースフローチャート

具体的な手順#

1. Xcode のブルートゥース権限を設定#

  1. General タブのFrameworks, Libraries, and Embedded Contentに CoreBluetooth.framework を追加します。以下の図のように:
    addCoreBluetoothFramework

  2. Signing & Capabilities タブのBackground ModesUses Bluetooth LE accessoriesにチェックを入れます。以下の図のように:

    bluetoothBackgroundModes

  3. Info タブのCustom iOS Target PropertiesPrivacy - Bluetooth Peripheral Usage DescriptionPrivacy - Bluetooth Always Usage Descriptionを追加します。

上記の手順を完了した後、Xcode のブルートゥース設定が完了します。それでは、ブルートゥースの初期化方法を見ていきましょう。

2. ブルートゥースの初期化呼び出し#

コードを見る前に、以下のマインドマップを見ておくと良いでしょう。これはiOS ブルートゥース知識の迅速な入門(詳細版)からのものです。

iOS ブルートゥース知識の迅速な入門(詳細版)

大まかな印象を得た後、右下の CoreBluetooth の使用方法を見ていきましょう。

CBCentralManagerを初期化します。CBCentralManagerはブルートゥースの初期化、スキャン、接続を担当します。初期化メソッドではブルートゥース権限の申請がポップアップ表示され、明示的に宣言する必要はありません。

 dispatch_queue_t queue = dispatch_queue_create("com.queue.bluetooth", DISPATCH_QUEUE_SERIAL);
 NSDictionary *dic = @{
     CBCentralManagerOptionShowPowerAlertKey: @YES,
     CBCentralManagerOptionRestoreIdentifierKey: @"com.queue.bluetooth"
 };
 self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:queue options:dic];

3. 周辺のブルートゥースデバイスをスキャン#

周辺のブルートゥースデバイスをスキャンします。CBCentralManagerが初期化された後、周辺のブルートゥースデバイスをスキャンするメソッドを呼び出し、ブルートゥースデバイスを発見します。
Ps: ブルートゥースデバイスに低電量のスリープ機能がある場合、ここでユーザーに手動でブルートゥースを先にアクティブにするように促すことができます。そうしないと接続が遅くなるか、接続できなくなることがあります。

 // スキャンを開始
- (void)startScan {
    // 既に発見されたデバイスを再スキャンしない
    NSDictionary *dic = @{
        CBCentralManagerScanOptionAllowDuplicatesKey: @NO,
        CBCentralManagerOptionShowPowerAlertKey: @YES
    };
     // スキャンを開始
    [self.centralManager scanForPeripheralsWithServices:nil options:dic];
}

また、近くに複数のブルートゥースデバイスが存在しない場合は、以下の方法を使用して、前回接続したデバイスに迅速に接続できます。retrieveConnectedPeripheralsWithServicesメソッドは、ブルートゥース接続に成功したデバイスを取得します。これらのデバイスは本 APP が接続したものではない可能性があるため、使用時には特に注意が必要です。

// スキャンを開始
- (void)startScan {
   // 既に発見されたデバイスを再スキャンしない
   NSDictionary *dic = @{
       CBCentralManagerScanOptionAllowDuplicatesKey: @NO,
       CBCentralManagerOptionShowPowerAlertKey: @YES
   };
    // スキャンを開始
    NSArray *arr = [self.centralManager retrieveConnectedPeripheralsWithServices:@[[CBUUID UUIDWithString:Target_SERVICE_UUID]]];
    if (arr.count > 0) {
        CBPeripheral *per = arr.firstObject;
        self.peripheral = per;
        [self.centralManager connectPeripheral:per options:nil];
    } else {
        // スキャンを開始
        [self.centralManager scanForPeripheralsWithServices:nil options:dic];
    }
}

4. 接続するブルートゥースデバイスを識別#

スキャンしたブルートゥースデバイスの処理。CBCentralManagerの初期化時にdelegateが設定されているため、CBCentralManagerDelegateのデリゲートメソッドを実装する必要があります。

その中でcentralManager:didDiscoverPeripheral:advertisementData:RSSI:メソッドはブルートゥースデバイスを発見した際のコールバックメソッドです。このメソッド内で接続するブルートゥースデバイスを識別し、接続メソッドを呼び出す必要があります。

ここで注意が必要なのは、iOS のブルートゥースではブルートゥースデバイスのMacアドレスを直接取得することができないため、デバイス側がブルートゥースの Mac アドレスをadvertisementDataに提供する必要があります。ここではデバイスメーカーと確認し、取得ロジックを把握する必要があります。例えば、advertisementDataのどのフィールドにMacアドレスが含まれているか、どの位置からどの位置までの値を取得するかを確認します。その後、対応するデータを取得し、16 進数の hex string に変換し、固定のルールに従ってMacアドレスを取得し、Macアドレスに基づいて接続するブルートゥースデバイスを特定します。

もちろん、簡単なブルートゥース名でフィルタリングした後、Macアドレスでさらに確認してユニークなデバイスを特定することもできます。接続したいデバイスが見つかったら、connectPeripheral:options:を呼び出して接続を開始します。

接続に成功したら、ブルートゥースデバイスのスキャンを停止し、ブルートゥースデバイスのデリゲートを設定し、サービスのスキャンを開始します。

#pragma mark - CBCentralManagerDelegate
- (void)centralManagerDidUpdateState:(CBCentralManager *)central {
   
}

// デバイスをスキャンした際
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *,id> *)advertisementData RSSI:(NSNumber *)RSSI {
   if (peripheral == nil) {
       return;
   }
   if((__bridge CFUUIDRef )peripheral.identifier == NULL) return;
   
   NSString *nameStr = peripheral.name;
   if(nameStr == nil || nameStr.length == 0) return;
   NSLog(@"nameStr: %@", nameStr);
   NSLog(@"advertisementData: %@", advertisementData);
   // 名前が指定のブルートゥースデバイス名を含むかどうかを判断
   if (([nameStr caseInsensitiveCompare:@"TargetBluetoothName"] == NSOrderedSame)){
        // 例としてブルートゥースアドレスは`advertisementData`の`kCBAdvDataManufacturerData`に格納されている
        // まず`kCBAdvDataManufacturerData`を取得
       NSData *manufacturerData = [advertisementData valueForKey:@"kCBAdvDataManufacturerData"];
       // そして16進数文字列に変換、このメソッドは後で提供します
       NSString *manufacturerDataStr = [BluetoothTool convertDataToHexStr:manufacturerData];
       if (manufacturerDataStr) {
            // そしてルールに従って`Macアドレス`を取得
           NSString *deviceMacStr = [manufacturerDataStr substringWithRange:NSMakeRange(x, 12)];
           // そして`取得したMacアドレス`と操作対象デバイスの`Macアドレス`が一致するかどうかを判断し、一致すれば接続
           if ([deviceMacStr caseInsensitiveCompare:targetDeviceMac] == NSOrderedSame) {
               [self.centralManager connectPeripheral:peripheral options:nil]; //接続コマンドを発行
               self.peripheral = peripheral;
           }
       }
   }
}

// 接続成功
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral {
   // 接続成功後、サービスを探す。nilを渡すとすべてのサービスを探します
   [self.centralManager stopScan];
   peripheral.delegate = self;
   [peripheral discoverServices:nil];
}

// 接続失敗
- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error {
   self.isConnected = NO;
   NSLog(@"接続失敗:%@", error.localizedDescription);
}

// 接続を切断
- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error {
   self.isConnected = NO;
   NSLog(@"接続切断:%@", error.localizedDescription);
}

5. 指定したブルートゥースデバイスのサービスをスキャン#

サービスのスキャン処理。上記でperipheral.delegateを設定したため、CBPeripheralDelegateのデリゲートメソッドを実装する必要があります。

peripheral:didDiscoverServices:はサービスを発見した際のコールバックで、このコールバックメソッド内で見つかったサービス UUID と接続するデバイスのサービス UUID(これはブルートゥースデバイスのメーカーが提供するか、デバイスの文書に記載されている)を一致させる必要があります。一致すれば、次のステップで特徴値を探します。

peripheral:didDiscoverCharacteristicsForService:error:は特徴を発見した際のコールバックで、読み取りと書き込みの特徴を取得するために使用されます。読み取りと書き込みの特徴も UUID で区別され、時には読み取りと書き込みが同じ UUID であることもあります。これもメーカーが提供するか、文書に記載されていることが多いです。Ps: ここで注意が必要なのは、メーカーが提供する文書に注意が必要で、一部のメーカーのデバイスでは特徴を取得した後、特定の情報を書き込む必要があり、指定された返答を取得しないと本当に接続成功とは言えないということです。

peripheral:didUpdateValueForCharacteristic:error:はブルートゥースデバイスがデータを返すコールバック、つまりデータを読み取るコールバックです。ここで注意が必要なのは、ブルートゥースの操作と通常のコマンドの実行が異なり、実行したらそれで終わりではないということです。ブルートゥースにコマンドを送信した後、ブルートゥースデバイスが返すデータに基づいてコマンドが成功したかどうかを判断する必要があります。複雑なロジックの大部分はこのメソッド内にあり、このメソッドが返すデータは Data 型であり、データを解読して Byte または Hex Str に変換して処理する必要があります。

peripheral:didWriteValueForCharacteristic:はコマンドが正常に書き込まれたかどうかのコールバックで、成功すれば指令がブルートゥースデバイスに正常に書き込まれたことを示します。つまり、ブルートゥースデバイスが指令を正常に受け取ったことを示しますが、指令が成功したかどうかは上記の返答データのメソッドで判断する必要があります。

コードは以下の通りです:

#pragma mark - CBPeripheralDelegate
// サービスを発見した際のコールバック
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(nullable NSError *)error {
   if (!error) {
       for (CBService *service in peripheral.services) {
           NSLog(@"serviceUUID:%@", service.UUID.UUIDString);
           
           if ([service.UUID.UUIDString caseInsensitiveCompare:TARGET_SERVICE_UUID] == NSOrderedSame) {
               // 特定のサービスの特徴値を発見
               [service.peripheral discoverCharacteristics:nil forService:service];
           }
       }
   }
}

// 特徴を発見した際のコールバック、特徴を発見するためにサービスを発見した(前のステップ)後に呼び出され、読み取りと書き込みの特徴を取得
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(nullable NSError *)error {
   for (CBCharacteristic *characteristic in service.characteristics) {
      // 時には読み書きの操作が1つのcharacteristicで完了することもある
      if ([characteristic.UUID.UUIDString caseInsensitiveCompare:TARGET_CHARACTERISTIC_UUID_READ] == NSOrderedSame) {
          self.read = characteristic;
          // 特徴の返信データを購読
          [self.peripheral setNotifyValue:YES forCharacteristic:self.read];
      } else if ([characteristic.UUID.UUIDString caseInsensitiveCompare:TARGET_CHARACTERISTIC_UUID_WRITE] == NSOrderedSame) {
          self.write = characteristic;
          // ここで注意が必要で、実際の状況に応じて特定の指令を実行する必要があるかどうかを確認し、接続成功を判断するために使用します
          [self makeConnectWrite];
      }
   }
}

// データを読み取った際のコールバック
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error {
   if (error) {
       NSLog(@"===読み取りエラー:%@",error);
       return;
   }
   
   if([characteristic.UUID.UUIDString caseInsensitiveCompare:Target_CHARACTERISTIC_UUID_READ] == NSOrderedSame){
        // 購読した特徴の返信データを取得
        // 返されたデータはData型であり、Dataを16進数の文字列に変換して処理するか、Byteに変換して処理します;
       NSString *value = [[BluetoothTool convertDataToHexStr:characteristic.value] uppercaseString];

        // 文書に従ってデータを解読または他の処理を行い、指定された判断ロジックに従って、指令が成功したかどうかを判断します

   }
}

// 書き込みが成功したかどうかのコールバック
- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error {
   if (error) {
       NSLog(@"===指令書き込みエラー:%@",error);
   }else{
       NSLog(@"===指令書き込み成功");
   }
}

6. 複数の指令を一括書き込み#

ブルートゥースデバイスが非同期をサポートせず、並行書き込みをサポートしない場合、複数の指令を一括書き込む必要がある際には注意が必要です。キューを作成し、キューの依存関係を設定する方法で、指令を順次 1 つずつ実行するように指定できます。

補助メソッド#

ほとんどの変換メソッドはIOS ブルートゥース通信におけるさまざまなデータ型間の変換からのもので、使用時に必要に応じて使用できます。

Data を 16 進数文字列に変換

ブルートゥースから返されたデータは NSData 型であり、以下のメソッドを呼び出して NSData を 16 進数文字列に変換し、文字列の特定の位置を処理します。
Ps: ここで注意が必要なのは、16 進数文字列に変換する際、後で算術演算を行う必要がある場合があるため、文字列に変換した後、統一して大文字に変換することをお勧めします。

  // NSDataを16進数の文字列に変換する、<0x00adcc asdfgwerf asdddffdfd> -> @"0x00adccasdfgwerfasdddffdfd"
+ (NSString *)convertDataToHexStr:(NSData *)data {
    if (!data || [data length] == 0) {
        return @"";
    }
    NSMutableString *string = [[NSMutableString alloc] initWithCapacity:[data length]];
    
    [data enumerateByteRangesUsingBlock:^(const void *bytes, NSRange byteRange, BOOL *stop) {
        unsigned char *dataBytes = (unsigned char*)bytes;
        for (NSInteger i = 0; i < byteRange.length; i++) {
            NSString *hexStr = [NSString stringWithFormat:@"%x", (dataBytes[i]) & 0xff];
            if ([hexStr length] == 2) {
                [string appendString:hexStr];
            } else {
                [string appendFormat:@"0%@", hexStr];
            }
        }
    }];
    return string;
}

10 進数を 16 進数文字列に変換、主にビット操作に使用され、String に変換してから String で Range を使用してビット操作を行うことができます。

NSString *hexStr = [NSString stringWithFormat:@"%02lx", (long)number];

16 進数文字列を 10 進数に変換、算術演算が必要な場合に使用され、まず文字列を 10 進数に変換し、演算後に再度 16 進数文字列に変換します。Ps: ここで変換する際、算術演算後の数字が 0 未満の場合、直接 10 進数を上記の方法で 16 進数文字列に変換して排他的論理和を取ると問題が発生します。

NSInteger num = strtoul(hexStr.UTF8String, 0, 16);

算術演算後に 0 未満の数字に対する特別な処理は以下の通りです:

NSInteger num = num - randNum;
if (num < 0) {
    // 数字が0未満の場合、256+この負数を使用し、結果を16進数文字列に変換します
    num = 256 + num;
}
NSString *hexStr = [[NSString stringWithFormat:@"%02lx", (long)num] uppercaseString];

文字列の排他的論理和メソッド

Data を文字列に変換したため、排他的論理和を行う際には文字列に対して排他的論理和を行う必要があります。参考にiOS で 2 つの同じ長さの文字列に対して排他的論理和を行うを参照し、長さの等しさの判断を削除し、ビットごとに排他的論理和を行います。

Ps: ここで注意が必要なのは負数のケースです。

+ (NSString *)xorPinvWithHexString:(NSString *)hexStr withPinv:(NSString *)pinv {
    NSString *resultStr;
    NSRange range = NSMakeRange(0, 2);
    for (NSInteger i = range.location; i < [hexStr length]; i += 2) {
        unsigned int anInt;
        NSString *hexCharStr = [hexStr substringWithRange:range];
        NSString *pinxStr = [BluetoothTool pinxCreator:hexCharStr withPinv:pinv];
        if (resultStr == nil) {
            resultStr = pinxStr;
        } else {
            resultStr = [NSString stringWithFormat:@"%@%@", resultStr, pinxStr];
        }
        range.location += range.length;
        range.length = 2;
    }
    return resultStr;
}

+ (NSString *)pinxCreator:(NSString *)pan withPinv:(NSString *)pinv
{
    if (pan.length != pinv.length)
    {
        return nil;
    }
    const char *panchar = [pan UTF8String];
    const char *pinvchar = [pinv UTF8String];
    
    NSString *temp = [[NSString alloc] init];
    
    for (int i = 0; i < pan.length; i++)
    {
        int panValue = [self charToint:panchar[i]];
        int pinvValue = [self charToint:pinvchar[i]];
        
        temp = [temp stringByAppendingString:[NSString stringWithFormat:@"%X",panValue^pinvValue]];
    }
    return temp;
}

+ (int)charToint:(char)tempChar
{
    if (tempChar >= '0' && tempChar <='9')
    {
        return tempChar - '0';
    }
    else if (tempChar >= 'A' && tempChar <= 'F')
    {
        return tempChar - 'A' + 10;
    }
    
    return 0;
}

CRC8 アルゴリズム

メーカーが提供するチェックアルゴリズムに注意が必要です。CRC8 チェックの場合、以下を参考にしてください。ただし、CRC8 maxin チェックであるかどうかにも注意が必要です。オンラインで試してみるのが最良です。以下のコードはiOS ブルートゥース開発における CRC8 チェックを参考にしており、CRC8 maxin チェックです。

// CRC8チェック
+ (NSString *)crc8_maxin_byteCheckWithHexString:(NSString*)hexString {
    NSString * tempStr = hexString;
    NSArray *tempArray = [self getByteForString:hexString];
        //    NSArray  * tempArray = [tempStr componentsSeparatedByString:@" "];//分隔符
    unsigned char testChars[(int)tempArray.count];
    for(int i=0;i<tempArray.count;i++){
        NSString * string = tempArray[i];
        unsigned char fristChar = [self hexHighFromChar:[string characterAtIndex:0]];
        unsigned char lastChar  = [self hexLowFromChar:[string characterAtIndex:1]];
        unsigned char temp = fristChar+lastChar;
        testChars[i] = temp;
    }
    unsigned char res = [self crc8_maxin_checkWithChars:testChars length:(int)tempArray.count];
    return [NSString stringWithFormat:@"%x", res];
}

+(unsigned char)hexHighFromChar:(unsigned char) tempChar{
    unsigned char temp = 0x00;
    switch (tempChar) {
        case 'a':temp = 0xa0;break;
        case 'A':temp = 0xA0;break;
        case 'b':temp = 0xb0;break;
        case 'B':temp = 0xB0;break;
        case 'c':temp = 0xc0;break;
        case 'C':temp = 0xC0;break;
        case 'd':temp = 0xd0;break;
        case 'D':temp = 0xD0;break;
        case 'e':temp = 0xe0;break;
        case 'E':temp = 0xE0;break;
        case 'f':temp = 0xf0;break;
        case 'F':temp = 0xF0;break;
        case '1':temp = 0x10;break;
        case '2':temp = 0x20;break;
        case '3':temp = 0x30;break;
        case '4':temp = 0x40;break;
        case '5':temp = 0x50;break;
        case '6':temp = 0x60;break;
        case '7':temp = 0x70;break;
        case '8':temp = 0x80;break;
        case '9':temp = 0x90;break;
        default:temp = 0x00;break;
    }
    return temp;
}

+(unsigned char)hexLowFromChar:(unsigned char) tempChar{
    unsigned char temp = 0x00;
    switch (tempChar) {
        case 'a':temp = 0x0a;break;
        case 'A':temp = 0x0A;break;
        case 'b':temp = 0x0b;break;
        case 'B':temp = 0x0B;break;
        case 'c':temp = 0x0c;break;
        case 'C':temp = 0x0C;break;
        case 'd':temp = 0x0d;break;
        case 'D':temp = 0x0D;break;
        case 'e':temp = 0x0e;break;
        case 'E':temp = 0x0E;break;
        case 'f':temp = 0x0f;break;
        case 'F':temp = 0x0F;break;
        case '1':temp = 0x01;break;
        case '2':temp = 0x02;break;
        case '3':temp = 0x03;break;
        case '4':temp = 0x04;break;
        case '5':temp = 0x05;break;
        case '6':temp = 0x06;break;
        case '7':temp = 0x07;break;
        case '8':temp = 0x08;break;
        case '9':temp = 0x09;break;
        default:temp = 0x00;break;
    }
    return temp;
}

+ (NSArray *)getByteForString:(NSString *)string {
  NSMutableArray *strArr = [NSMutableArray array];
  for (int i = 0; i < string.length/2; i++) {
      NSString *str = [string substringWithRange:NSMakeRange(i * 2, 2)];
      [strArr addObject:str];
  }
  return [strArr copy];
}

16 進数文字列を Data に変換

このメソッドはブルートゥースにコマンドを送信するために使用されます。すべてのロジックは 16 進数文字列として処理されますが、ブルートゥースデバイスは Data のみを受け取るため、16 進数文字列を Data に変換してからブルートゥースに送信する必要があります。

Ps: ここでも文字列を大文字に変換してから Data に変換することをお勧めします。

// 16進数の文字列をNSDataに変換する、渡された文字列は128ビットの文字に変換され、不足しているビットは数字で補完されます。必要に応じて位置を切り取ることができます。 @"0a1234 0b23454" -> <0a1234 0b23454>
+ (NSData *)convertHexStrToData:(NSString *)hexStr {
    if (!hexStr || [hexStr length] == 0) {
        return nil;
    }
    
    NSMutableData *hexData = [[NSMutableData alloc] initWithCapacity:20];
    NSRange range;
    if ([hexStr length] % 2 == 0) {
        range = NSMakeRange(0, 2);
    } else {
        range = NSMakeRange(0, 1);
    }
    for (NSInteger i = range.location; i < [hexStr length]; i += 2) {
        unsigned int anInt;
        NSString *hexCharStr = [hexStr substringWithRange:range];
        NSScanner *scanner = [[NSScanner alloc] initWithString:hexCharStr];
        
        [scanner scanHexInt:&anInt];
        NSData *entity = [[NSData alloc] initWithBytes:&anInt length:1];
        [hexData appendData:entity];
        
        range.location += range.length;
        range.length = 2;
    }
    return hexData;
}

踏み外し#

  • ブルートゥースの初期化がクラッシュし、Assertion failure in -[CBCentralManager initWithDelegate:queue:options:], CBCentralManage...

    新しいプロジェクトでブルートゥース権限が有効になっていないためです。Project -> Target -> Signing & Capabilities の Background Modes でUse Bluetooth LE accessoriesにチェックを入れるだけです。以下の図のように:
    bluetoothcrash

  • 複数のデバイスの接続切り替えが混乱する

    複数のデバイスを行き来する際に混乱が発生することがあります。つまり、元々接続していたブルートゥースデバイス 1 に対して、ブルートゥースデバイス 2 に指令を送信した結果、指令がブルートゥースデバイス 1 に対して操作されることです。最初は接続を切断するメソッドを呼び出していないか、切断の時間が不十分だと思っていました。調査の結果、retrieveConnectedPeripheralsWithServicesを使用したためであることが判明しました。接続を切断した後、再接続する際に、retrieveConnectedPeripheralsWithServicesで取得した最初のデバイスは、直前に切断したデバイスであるため、再接続時に誤ったブルートゥースデバイスに接続されてしまいます。

  • 排他的論理和の結果が誤っている

    開発中に、ロジックや暗号化アルゴリズムに問題がない場合でも、指令が無効になることが偶にあります。最初はブルートゥースデバイスの問題だと思っていましたが、一部の指令は成功し、一部は失敗することが判明しました。調査の結果、アルゴリズムに関与する算術演算部分で負数が発生した場合、指令が失敗することがわかりました。さらに調査した結果、負数を 16 進数に変換して排他的論理和を計算する際に問題が発生することがわかりました。解決策は、負数が発生した場合、(256 + 負数) を使用して正の値に変換し、その後 16 進数に変換して排他的論理和を計算することです。

  • アプリがバックグラウンドに入ると、ユーザーから以下の情報が提示される

    『xxx』は新しい接続のためにブルートゥースを使用したいと考えています。設定で新しい接続を許可できます。

    最初はバックグラウンドでブルートゥースの活動があると思っていましたが、調査の結果、バックグラウンドに入るとブルートゥースの接続を切断するメソッドが呼び出されることがわかりました。したがって、バックグラウンド活動の問題ではありません。ユーザーとコミュニケーションを取った結果、ユーザーのブルートゥーススイッチがオフになっていることがわかりました。バックグラウンドに入るとこのメッセージが表示され、オンにするとこの問題は発生しません。接続を切断するメソッド内で初期化されたCBCentralManagerをデフォルトで使用しているため、ブルートゥーススイッチがオンになっているかどうかを判断していませんでした。

まとめ#

ブルートゥースデバイスに接続する際には、まず Xcode でブルートゥース権限を設定し、次にデバイスメーカーが提供する文書を通読し、特にブルートゥースデバイスの Mac アドレスがどのように提供されるか、ブルートゥースデバイスのサービス UUID と読み書き UUID が提供されているか、ブルートゥースが接続成功したかどうかを判断する方法、指令の暗号化と復号化方法などに注意を払う必要があります。その後、システムが提供する方法でブルートゥースを初期化し、ブルートゥース操作指令のメソッドと暗号化および復号化メソッドを封装します。最後に、すべてが完了したら、ブルートゥースデバイスの接続を切断することを忘れないでください。

参考文献#

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。