iOS藍牙開發筆記(LightBlue調試、大小端轉換、進制轉換)

本文記錄下在項目開發過程中遇到的問題及解決問題使用的方法

1、用LightBlue調試(或者nRF Connect
2、找出特征的properties組成
3、數據的大小端轉換
4、數據的進制轉換
5、藍牙數據傳輸的網絡工具類

1、LightBlue

不管你是開發安卓還是iOS,在你還沒寫有關藍牙的代碼之前,你必須確認一件事:這個硬件是好使的。
所以你需要一個第三方的APP來調試確認。
工具有很多種,比如LightBlue、nRF Connect。
用法大同小異,下面以LightBlue為例。

下載,進入APP后,打開藍牙,下拉刷新就可以看到附近的外設了,找到你們硬件工程師提供的外設名稱,點擊進入;

主頁面

然后找到相應的UUID(大字體的是服務的UUID,可點擊的UUID是服務的特征),會一同看到Properties屬性值:比如 Read 、Write 、Write Without Response Notify等 。

外設頁面

然后點擊需要的服務UUID,如果是訂閱特征的話,會有“Listen for notifications”按鈕可以點擊,如果點擊不了就說明不是訂閱特征,硬件的這個UUID是發不出數據的。
如果是寫特征的話,會有 “Write new value”是向硬件寫命令的。如控制開關等等。

特征頁面

有LightBlue與硬件工程師一同調試硬件很方便,當問題出現時,你可以確認是硬件的問題,還是你自己代碼的問題。

而且當我們作為iOS開發藍牙硬件的時候,你的硬件工程師并不知道他提供給你的硬件的服務是什么,特征是什么,只會跟你說:“這個UUID是板子收數據的,這個UUID是板子發數據的”,所以我們需要找到這些具體的UUID,然后辨別哪個可以實現功能。


2、找出特征的properties組成

當我們打印一個特征時,會發現properties=0x14,我們還可以使用以下方法在實現藍牙代理方法里找出特征的properties組成。

//找出特征的properties組成
//http://lecason.com/2015/08/19/Objective-C-Find-Conbine/
-(void)logCharacteristicProperties:(CBCharacteristicProperties)properties
{
    if (properties & CBCharacteristicPropertyBroadcast) {
        NSLog(@"CBCharacteristicPropertyBroadcast");
    }
    if (properties & CBCharacteristicPropertyRead) {
        NSLog(@"CBCharacteristicPropertyRead");
    }
    if (properties & CBCharacteristicPropertyWriteWithoutResponse) {
        NSLog(@"CBCharacteristicPropertyWriteWithoutResponse");
    }
    if (properties & CBCharacteristicPropertyWrite) {
        NSLog(@"CBCharacteristicPropertyWrite");
    }
    if (properties & CBCharacteristicPropertyNotify) {
        NSLog(@"CBCharacteristicPropertyNotify");
    }
    if (properties & CBCharacteristicPropertyIndicate) {
        NSLog(@"CBCharacteristicPropertyIndicate");
    }
    if (properties & CBCharacteristicPropertyAuthenticatedSignedWrites) {
        NSLog(@"CBCharacteristicPropertyAuthenticatedSignedWrites");
    }
    if (properties & CBCharacteristicPropertyExtendedProperties) {
        NSLog(@"CBCharacteristicPropertyExtendedProperties");
    }
    if (properties & CBCharacteristicPropertyNotifyEncryptionRequired) {
        NSLog(@"CBCharacteristicPropertyNotifyEncryptionRequired");
    }
    if (properties & CBCharacteristicPropertyIndicateEncryptionRequired) {
        NSLog(@"CBCharacteristicPropertyIndicateEncryptionRequired");
    }
}

3、數據的大小端轉換

當你拿到了外設的數據,向服務器傳的時候,你的后臺開發java小伙伴可能會跟你說傳過來的數據都不對勁,很大之類的,你可以做下數據的大小端轉換試一試。

這是為什么呢?

在幾乎所有的機器上,多字節對象都被存儲為連續的字節序列。例如在C語言中,一個類型為int的變量x地址為0x100,那么其對應地址表達式&x的值為0x100。且x的四個字節將被存儲在存儲器的0x100, 0x101, 0x102, 0x103位置。

而存儲地址內的排列則有兩個通用規則。一個多位的整數將按照其存儲地址的最低或最高字節排列。如果最低有效字節在最高有效字節的前面,則稱小端序;反之則稱大端序。在網絡應用中,字節序是一個必須被考慮的因素,因為不同機器類型可能采用不同標準的字節序,所以均按照網絡標準轉化。

例如假設上述變量x類型為int,位于地址0x100處,它的十六進制為0x01234567,地址范圍為0x100~0x103字節,其內部排列順序依賴于機器的類型。大端法從首位開始將是:0x100: 01, 0x101: 23,..。而小端法將是:0x100: 67, 0x101: 45,..

網絡傳輸一般采用大端序,也被稱之為網絡字節序,或網絡序。IP協議中定義大端序為網絡字節序。伯克利socket API定義了一組轉換函數,用于16和32bit整數在網絡序和本機字節序之間的轉換。htonl,htons用于本機序轉換到網絡序;ntohl,ntohs用于網絡序轉換到本機序。

使用這些函數決定于你要從主機字節順序(你的電腦上的)還是網絡字節順序轉化。如果是"host",函數的第一個字母為"h",否則"network"就為"n"。函數的中間字母總是"to",因為你要從一個轉化到另一個,倒數第二個字母說明你要轉化成什么。最后一個字母是數據的大小,"s"表示short,"l"表示long。

htons() host to network short
htonl() host to network long
ntohs() network to host short
ntohl() network to host long

如果不想用c函數,用NSSwapHostIntToBig() NSSwapHostShortToBig() NSSwapHostLongToBig()等等也可以,看起來更加清晰明了,但其實里面也是封裝的上面這些c函數。

//使用例子:
short number = 2;
number = ntohs(number);
number =  NSSwapHostShortToBig(number);

當你要傳輸的函數不僅僅是簡單的short,int,long型,比如說是3字節類型的數據,需要以下自定義的大小端轉換方法

//大端與小端互轉
-(NSData *)dataTransfromBigOrSmall:(NSData *)data
{
    NSString *tmpStr = [self dataChangeToString:data];
    NSMutableArray *tmpArra = [NSMutableArray array];
    for (int i = 0 ;i<data.length*2 ;i+=2)
 {
        NSString *str = [tmpStr substringWithRange:NSMakeRange(i, 2)];
        [tmpArra addObject:str];
    }
    NSArray *lastArray = [[tmpArra reverseObjectEnumerator] allObjects];
    NSMutableString *lastStr = [NSMutableString string];
    for (NSString *str in lastArray)
 {
        [lastStr appendString:str];   
    }
    NSData *lastData = [self HexStringToData:lastStr];
    return lastData; 
}

-(NSString*)dataChangeToString:(NSData*)data
{
    NSString * string = [NSString stringWithFormat:@"%@",data];
    string = [string stringByReplacingOccurrencesOfString:@"<" withString:@""];
    string = [string stringByReplacingOccurrencesOfString:@">" withString:@""];
    string = [string stringByReplacingOccurrencesOfString:@" " withString:@""];
    return string;  
}

-(NSMutableData*)HexStringToData:(NSString*)str
{
    NSString *command = str;
    command = [command stringByReplacingOccurrencesOfString:@" " withString:@""];
    NSMutableData *commandToSend= [[NSMutableData alloc] init];
    unsigned char whole_byte;
    char byte_chars[3] = {'\0','\0','\0'};
    int i;
    for (i=0; i < [command length]/2; i++) {
        byte_chars[0] = [command characterAtIndex:i*2];
        byte_chars[1] = [command characterAtIndex:i*2+1];
        whole_byte = strtol(byte_chars, NULL, 16);
        [commandToSend appendBytes:&whole_byte length:1];
    }
    return commandToSend;
}

查看本機是大端還是小端的方法:

    short int x;
    char x0,x1;
    x=0x1122;
    x0=((char*)&x)[0]; //低地址單元
    x1=((char*)&x)[1]; //高地址單元
    
    if (x0 == 0x11) {
        NSLog(@"大端");
    }
    else if (x0==0x22)
    {
        NSLog(@"小端");
    }

或者是這樣:

    if (1 != htonl(1)) {
        //小端模式,作相應處理
        NSLog(@"小端");
    } else {
        //大端模式,作相應處理
        NSLog(@"大端");
    }

4、數據的進制轉換

當我第一次從外設拿到數據,在- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error中直接打印出來,是被<>包裹的十六進制數據,而后臺一般需要的是十進制的,比較好處理。
我試了幾個函數,想直接把收到外設的值轉為字符串,發現都不可以

1、為空

NSString *result = [[NSString alloc] initWithData:characteristic.value  encoding:NSUTF8StringEncoding];
NSLog(@"%@",result);

2、為空

 NSString *result = [NSString stringWithUTF8String:[characteristic.value bytes]];
NSLog(@"%@",result);

3、亂碼+空

NSStringEncoding myEncoding = CFStringConvertEncodingToNSStringEncoding (kCFStringEncodingGB_18030_2000);
NSString *result=[[NSString alloc]initWithData:characteristic.value encoding:myEncoding];
NSLog(@"%@",result);

最終我的數據傳輸方案是在該代理方法中,將接收到外設的數據NSData轉為十六進制,再轉為十進制,然后大小端轉換,最后轉為NSData發給服務器。

下面列出我使用的進制轉換工具:
//10進制int轉NSData
-(NSData *)intToData:(int)i;

//NSData轉int(10進制)
-(int)dataToInt:(NSData *)data;

//將NSData轉化為16進制字符串
- (NSString *)convertDataToHexStr:(NSData *)data;

//16進制字符串轉10進制int
- (int)hexNumberStringToNumber:(NSString *)hexNumberString;

//16進制字符串轉NSData
- (NSData *)hexToBytes:(NSString *)str;

//普通字符串,轉NSData
- (NSData *)stringToBytes:(NSString *)str;

//int轉data
-(NSData *)intToData:(int)i
{
    NSData *data = [NSData dataWithBytes: &i length: sizeof(i)];
    return data;
}


//data轉int
-(int)dataToInt:(NSData *)data
{
    int i;
    [data getBytes:&i length:sizeof(i)];
    return i;
}


/將NSData轉化為16進制字符串
- (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;
}


//16進制字符串轉10進制
- (int)hexNumberStringToNumber:(NSString *)hexNumberString
{
    NSString * temp10 = [NSString stringWithFormat:@"%lu",strtoul([hexNumberString UTF8String],0,16)];
    //轉成數字
    int cycleNumber = [temp10 intValue];
    return cycleNumber;
}


//16進制字符轉(不帶0x),轉NSData
-(NSData *)hexToBytes:(NSString *)str
{
    NSMutableData * data = [NSMutableData data];
    for (int i = 0; i+2 <= str.length; i+=2) 
{
        NSString * subString = [str substringWithRange:NSMakeRange(i, 2)];
        NSScanner * scanner = [NSScanner scannerWithString:subString];
        uint number;
        [scanner scanHexInt:&number];
        [data appendBytes:&number length:1];
    }
    return data.copy;
}


//普通字符串,轉NSData
- (NSData *)stringToBytes:(NSString *)str
{
    return [str dataUsingEncoding:NSASCIIStringEncoding];
}

關于數據的進制轉換網上一搜有很多,我也是整理一下我用到的,一般也就是十進制、十六進制、NSData的互轉,希望對你有幫助。


5、藍牙數據傳輸的網絡工具類

我的服務器端要求數據是以二進制流的形式,一段時間一個包發給他,而不是json形式的字典,所以我自定義了網絡工具類,用NSMutableData配置請求體,而不是直接用AFN了事。

#import <Foundation/Foundation.h>

@interface JDBLENetworkTool : NSObject

//請求體
@property (nonatomic, strong) NSMutableData *bodyData;

+ (instancetype)sharedInstance;
//拼3字節數據段
- (void)append3BytesDataWith:(int)number;
//拼2字節數據段
- (void)appendShortDataWith:(short)number;
//拼int型數據段
- (void)appendIntDataWith:(int)number;

- (void)postForPath:(NSString *)path
            success:(void (^) (NSDictionary *data))success
             failed:(void (^) (NSDictionary *failData))fail;

@end
#import "JDBLENetworkTool.h"
#import "OSZDataTool.h"

@implementation JDBLENetworkTool

+ (instancetype)sharedInstance
{
    static JDBLENetworkTool *tool = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        tool = [[JDBLENetworkTool alloc]init];
    });
    return tool;
}


- (void)append3BytesDataWith:(int)number
{
    Byte bytes[3];
    memcpy(bytes ,&number ,sizeof(bytes));
    NSData *contentData = [NSData dataWithBytes: &bytes length: sizeof(bytes)];
    NSData *bigData = [[OSZDataTool sharedTool] dataTransfromBigOrSmall:contentData];
    [self.bodyData appendData:bigData];
}


- (void)appendShortDataWith:(short)number
{
    number = ntohs(number);
    NSData *bigData = [NSData dataWithBytes: &number length: sizeof(number)];
    [self.bodyData appendData:bigData];
}

- (void)appendIntDataWith:(int)number
{
//    number =  NSSwapHostIntToBig(number);
    number = ntohl(number);
    NSData *bigData = [NSData dataWithBytes: &number length: sizeof(number)];
    [self.bodyData appendData:bigData];
}

- (void)postForPath:(NSString *)path
            success:(void (^) (NSDictionary *data))success
             failed:(void (^) (NSDictionary *failData))fail
{
    // 1. 獲取服務器端口的地址
    NSURL *url = [NSURL URLWithString:path];
    //#warning:對于POST請求,必須手動設置其請求方法,因此要使用可變請求
    // 2. 創建請求
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    // 3. 設置請求方式
    request.HTTPMethod = @"POST";
    [request setValue:@"application/octet-stream" forHTTPHeaderField:@"Content-Type"];
    // 4. 設置請求體
    request.HTTPBody = self.bodyData;
    
    [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        NSLog(@"%@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
    }] resume];
}

然后在didUpdateValueForCharacteristic這個代理方法中用這個網絡工具類拼接請求體,達到一定數量時發送就可以了。

希望對你有幫助,給個喜歡吧:-D

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容