カテゴリー「Bluetooth」の記事

自宅iBeaconの改良

今回は小ネタですが、以前「REBL600FRで自宅iBeacon」で書いた、自宅iBeaconの改良記事です。当初の自宅iBeaconは外出(exit region)・帰宅(enter region)で「いってらっしゃい」「おかえりなさい」のメッセージを投げるだけでした。今回、帰宅時にWiFiがOFFの場合は、ONにするよう通知する機能を追加しました。

2014/10/3追記:iOS 8への対応を追記しました。


機能追加の目的

私はau iPhone5を使っているのですが、通勤路線の駅毎にau WiFiを掴んでくれます。一時期掴まなくなっていたのですが、最近なぜか復活しました。多くの方が感じていると思いますが、これ、ハッキリ言って『迷惑』です。たいがい品質の悪いWiFiを掴み停車中はLTEに比べてアクセス速度がスローダウン、電車が動き出してもしばらくWiFiを離してくれない(モバイル/LTEに切り替えてくれない)ためアクセスが止まってしまいます。そのため、電車に乗っている間は通常WiFiをOFFにしています。

家に帰ったときWiFiをONにし忘れていると、そういうときに限って、子供がモバイル/LTEの状態でYou Tubeを使い倒してパケットカウントを上げてくれることがあります・・・まあ、7Gの上限に到達することはないのですが、家にいるときはWiFiを使った方が速度も速いし、キャリアにも優しいので、家ではWiFiをONにするのを忘れないようにしたいものです。

そこで、iBeaconと連動して、家に帰ったときにWiFiがOFFの場合は、ONにするよう通知を出すようにしました。通知のイメージはこんな感じです。

IMG_0518

下から2行目の「おかえりなさい」時はWiFiがONなのでそのまま、上から2行目の「おかえりなさい」時はWiFiがOFFのため、「WiFiをOnにしましょう」と追加メッセージを投げています。


実現方式

最初は、iOSでWiFiのOn/Off状態を取得するAPIがないか調べてみたのですがなさそうです。On/Offの設定をするのは当然、脱獄(Jail Break)しないと無理とstackOverflowにも書いてあります。iOSの場合、インタフェースの状態を調べるAPIはなさそうなので、他の方法として、Reachabilityクラスを使うことにしました(使い方は、このサイトを参考にしました)。

Reachabilityクラスは、iOSでネット接続の状態を調べることができるクラスで、WiFi接続状態、モバイル(3G/4G)接続状態、未接続状態などを取得できます。間接的な方法ですが、家に帰ったとき(iBeacon regionに入ったとき)Reachability状態がモバイル接続の場合、WiFiがOFFであると判定します。

 

コード

コードを以下に示します。

//
//  ViewController.m
//  Bea子v2
//
//  Created by Todotani on 2014/05/11.
//

#import "ViewController.h"
#import "Reachability.h"

@interface ViewController ()

@property (strong, nonatomic) CLLocationManager *locationManager;
@property (strong, nonatomic) NSUUID *proximityUUID;
@property (strong, nonatomic) CLBeaconRegion *beaconRegion;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    if ([CLLocationManager isMonitoringAvailableForClass:[CLCircularRegion class]]) {
        self.locationManager = [[CLLocationManager alloc] init];
        self.locationManager.delegate = self;
        
        self.proximityUUID = [[NSUUID alloc] initWithUUIDString:@"03BA6107-9F0A-4461-B1D8-504DA65632DD"];
        self.beaconRegion = [[CLBeaconRegion alloc] initWithProximityUUID: self.proximityUUID
                                                               identifier:@"com.todotani.ibeacon"];
        [self.locationManager startMonitoringForRegion: self.beaconRegion];
    } else {
        //iBeaconが利用できないOS, Deviceの場合
        NSLog(@"お使いの端末ではiBeaconを利用できません。");
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"確認"
                                                        message:@"お使いの端末ではiBeaconを利用できません。"
                                                       delegate:self
                                              cancelButtonTitle:nil
                                              otherButtonTitles:@"OK", nil];
        [alert show];
    }
}


// 指定した領域に入った場合
- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region
{
    [self sendLocalNotificationForMessage:@"おかえりなさい、ご主人様"];
    [NSThread sleepForTimeInterval:5.0];
    Reachability *reachablity = [Reachability reachabilityForInternetConnection];
    NetworkStatus status = [reachablity currentReachabilityStatus];
    if (status == ReachableViaWWAN) {
        [self sendLocalNotificationForMessage:@"WiFiをOnにしましょう"];
    }
}

// 指定した領域から出た場合
- (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region
{
    [self sendLocalNotificationForMessage:@"いってらっしゃい、ご主人様"];
}

#pragma mark - Private methods

- (void)sendLocalNotificationForMessage:(NSString *)message
{
    UILocalNotification *localNotification = [UILocalNotification new];
    localNotification.alertBody = message;
    localNotification.fireDate = [NSDate date];
    localNotification.soundName = UILocalNotificationDefaultSoundName;
    [[UIApplication sharedApplication] scheduleLocalNotification:localNotification];
}

@end
51行目(Reachabilityインスタンスの生成)~55行目までがWiFi Off判定のために追加した部分です。

50行目に5秒のsleepが入れてあるのは、家に帰った際に自宅WiFiに接続される前に53行目の条件判定が動いて、WiFi ONの場合でもOFFと誤判定するのを回避するためです。5秒のsleepを入れることで、WiFiがONの場合は確実に接続されてから条件判定を行います。sleepを入れるのは、汚いコードで気に入らないのですが、仕方なく妥協。


iOS 8への対応

2014/10/3追記:iPhoen 6の発売早々に機種変してしまいました(キャリアも訳あってドコモにチェンジ)。iPhone 6設定後「Be子 App」を再度入れても動いてくれません。iOS 8から位置情報取得設定(iBeaconを使う場合は位置情報取得をOnにする必要があります)の方法が変わっていますが、設定 → プライバシー → 位置情報サービスから「Be子 App」の位置情報取得を「常に許可」にしても勝手に「許可しない」に戻されてしまい、iBeaconが検知できません。Twitterで症状を流したら、@tw_hoehoeさんから、リンクのblogに記載がある問題であることを教えてもらいました。このblogによると、常時位置情報取得を許可するためにはinfo.plistを編集して、”NSLocationAlwaysUsageDescription”エントリを作り、”This app use iBeacon Loacation”などの適当なString Valueを設定する必要があります。

iOS 8になって、設定条件が変わった箇所に当たってしまったということでした。


我が家のiBeaconたち

当初は、ランニングエレクトロニクスさんのREBL600FR 1個で運用していましたが、部屋を移動するとiBeaconが切れ、「いってらっしゃいませ」「おかえりなさい」を繰り返すフラップ状態になることがあったため、現在はスイッチサイエンスさんのmbed HRM1017との2台体制にしています。BL600FRは電池駆動を生かして、電源が取れない居間の壁に貼り付けてあり、HRM1017はパソコン部屋においてACアダプタから電源を取っています。

Friskケースに入れて壁に貼り付けたREBL600FR

IMG_0521

 

机の上に置いた、mbed HRM1017

IMG_0523

REBL600FRで自宅iBeacon

RunningElectronicsさんからFRISKケースサイズのBL600基板、REBL600FRを購入して自宅iBeaconを作ってみました。

2014/10/3追記:自宅iBeaconを運用してみると、どうも電池の持ちが悪く(2ヶ月程度で電池が消耗)、iBeaconのパラメーターをチューニングしました。

 

デフォルト動作

購入品には、シリアル接続用のL字ピンヘッダや電池(CR2032)が付属しています。FRISKケースに入れてみるとこんな感じ(ピンヘッダをつけると、ケースの突起を切り取る工作が必要)。

IMG_0395

 

デフォルトでiBeaconアプリが書き込まれており、IDもAppleのものなので、モジュールの電源をONにして(mode SWはRUN mode)、iPhoneにLocate for iBeacon Appをインストールすると以下のようにiBeaconの信号を検出します。

IMG_0396

 

Beacon IDの書き換え

自宅iBeacon用にBeacon IDを書き換えてみます。まず、REBL600FRとPCをシリアル接続。スイッチサイエンスさんのFTDI USBシリアル変換アダプターを使用するとピン配置があわせてあるため簡単に接続ができます。接続の際は、ボードの電源SWをOFFにしてモード切替SWをDEV modeにしてUSBアダプターから給電します。

IMG_0411

次に、BL600製造元のLaird社Webから、UwTerminalをDLします(執筆時点はVer.6.80)。適当なディレクトリに展開してUwTerminal.exeを起動。COMポートを選択し、Baudrateを9600に変更します。HandshakingはデフォルトのCTS/RTSでもよいのですが(ただし、この場合は、CTS/DTR信号もクロス接続が必要)、Noneを選んでいます。

UwTerminalConf

 

OKボタンを押すと、Terminal画面に切り替わります。AT+EnterでOOが帰ってくれば接続成功です。AT I 3でファームウェアのバージョン、AT+DIRで書き込まれているソフトファイルが出力されます(デフォルトでは、$autorun$がiBeaconアプリとして書き込まれています)。

UwTerminalTerm

 

次に、Lairdの”Download the smartBASIC iBeacon Application file”をダウンロード。$autorun$.iBeacon.Firmware_v1_3_57_19_or_Older.sbファイルのID定義部分を書き換えます。自宅用のID生成はOSXのuuidgenコマンドを使っています。

47行目に新しく生成したUUIDを設定、49, 51行目のMajor/Minor valueは0を設定しています。

2014/10/3追記:電池の持ちが悪い原因として、デフォルトの設定では、送信パワー(53行目のBEACON_TXPOWER_DBM)が4dBm、Beaconの送信間隔(70行目のADV_INTERVAL_MS)が200msになっていることが考えられます。そこで、送信パワーを-8dBm、送信間隔を600msにしてみました。送信間隔を1000msまで広げると、Locate iBacon Appで監視した際に状態がunknownになる頻度が高く、在室中でもExitRegionを検出しそうだったため、600msに留めています。この設定でどの程度電池寿命が延びるかを監視する予定です。

'//******************************************************************************
'// Laird Technologies (c) 2013
'//
'// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
'// +++++                                                                      ++
'// +++++  When UwTerminal downloads the app it will store it as a filename   ++
'// +++++  which consists of all characters up to the first . and excluding it ++
'// +++++                                                                      ++
'// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
'//
'// $autorun$.iBeacon.sb
'//
'//
        '//version of this app in Device Info Service
#define APP_VERSION             "2.1.0"
'//
'//**************************************************************************************
'//**** Uncomment the first #define line for firmware older than 1.3.57.19 and older ****
'//**************************************************************************************
#define HANDLERCHARVAL HndlrCharVal(ByVal charHndl)  //for <= v1.3.57.19  
'//#define HANDLERCHARVAL HndlrCharVal(ByVal charHndl, ByVal offset, ByVal len)
'//
'//   ###################################################################################
'//   #     Version history in reverse chronological order (Customer change history)    #
'//   ###################################################################################
'//   #                                                                                 #
'//   #  -----------------------------------------------------------------------------  #
'//   #  ::Changelog:: 05/12/13 -> 2.1.0                                                #
'//   #  -----------------------------------------------------------------------------  # 
'//   #                                                                                 #
'//   ###################################################################################
'//
'//
'//   ###################################################################################
'//   #                                                                                 #
'//   # See file $autorun$.iBeacon.sblib for usage instructions                         #
'//   #                                                                                 #
'//   ###################################################################################
'//    
'//
'//******************************************************************************

'//******************************************************************************
'// Definitions that customers use to customise the source
'//******************************************************************************
        '// New Uuid for beacon  03BA6107-9F0A-4461-B1D8-504DA65632DD
#define BEACON_UUID                          "\03\ba\61\07\9f\0a\44\61\b1\d8\50\4d\a6\56\32\dd"
        '// Default Major value for beacon (16 bit number)
#define BEACON_MAJOR                         0x0000
        '// Default Minor value for beacon (16 bit number)
#define BEACON_MINOR                         0x0000
        '// Default TxPower, one of (4,0,-4,-8,-12,-16,-20,-40,-50) 
#define BEACON_TXPOWER_DBM                   -8
        '//RSSI at 1m from BL600 when tx power = -8
        '//Set this by advertising at -8 dBm and measuring the RSSI 1m from the BL600
#define CALIBRTD_VALUE_DBM                   -60
        '// Default value for iBeacon format
        '//   --> 0       : Full AD
        '//   --> 1       : UUID + Major + Minor
        '//   --> 2       : UUID
        '//   --> Default : 0
#define BEACON_FORMAT                        0

        '// Default time to remain connectible : 1-255
#define REMAIN_CONN_TME_SEC                  20
        '// min time to remain connectible when updated over the air : 3-255 
#define REMAIN_CONN_TME_SEC_MIN              3

        '//Default advertising interval -- higher the number, lower the current consumption
#define ADV_INTERVAL_MS                      600  //should be in range 100..5000
        '//Advertise time out (0==forever)
#define ADV_TIMEOUT_MINUTES                  0
ソースを別の名前で保存(先頭は$autorun$.とすること、私は、$autorun$.iBeacon.Firmware_v1_3_57_19_or_Older.mod.sbという名前にしています)。

UwTerminal画面を右クリックするとメニューが表示されますので、XCompile + Loadを選択。ファイル選択ダイアログが出ますので、先ほど保存したファイルを指定します。

UwTerminalMenu

 

SmartBASICのソースがバイトコードにコンパイルされ、モジュールに転送されます。これでIDの変更完了。

 

BL600_NewID

 

iBeacon検知アプリの作成

Locate iBでもiBeacon圏内に入るとiOSの「通知センター」に表示をプッシュすることができますが、”Enter Region”とかで味気がありません。せっかくの自宅iBeaconなので、もう少し萌えるメッセージを出したいということで、以下のような簡単なアプリを作ってみました。コードは、Takahiro Octopress Blogさんから拝借しました。iOSは7.1.1, Xcode 5.1.1を使用しています。

//
//  ViewController.h
//  Bea子
//
//  Created by Todontai on 2014/05/11.
//

#import <UIKit/UIKit.h>
#import <CoreLocation/CoreLocation.h>

@interface ViewController : UIViewController<CLLocationManagerDelegate>

@end
//
//  ViewController.m
//  Bea子
//
//  Created by Todotani on 2014/05/11.
//

#import "ViewController.h"

@interface ViewController ()

@property (strong, nonatomic) CLLocationManager *locationManager;
@property (strong, nonatomic) NSUUID *proximityUUID;
@property (strong, nonatomic) CLBeaconRegion *beaconRegion;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    if ([CLLocationManager isMonitoringAvailableForClass:[CLCircularRegion class]]) {
        self.locationManager = [[CLLocationManager alloc] init];
        self.locationManager.delegate = self;
        
        self.proximityUUID = [[NSUUID alloc] initWithUUIDString:@"03BA6107-9F0A-4461-B1D8-504DA65632DD"];
        self.beaconRegion = [[CLBeaconRegion alloc] initWithProximityUUID: self.proximityUUID
                                                               identifier:@"com.todotani.ibeacon"];
        [self.locationManager startMonitoringForRegion: self.beaconRegion];
    } else {
        //iBeaconが利用できないOS, Deviceの場合
        NSLog(@"お使いの端末ではiBeaconを利用できません。");
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"確認"
                                                        message:@"お使いの端末ではiBeaconを利用できません。"
                                                       delegate:self
                                              cancelButtonTitle:nil
                                              otherButtonTitles:@"OK", nil];
        [alert show];
    }
}


// 指定した領域に入った場合
- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region
{
    [self sendLocalNotificationForMessage:@"おかえりなさい、ご主人様"];
}

// 指定した領域から出た場合
- (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region
{
    [self sendLocalNotificationForMessage:@"いってらっしゃい、ご主人様"];
}

#pragma mark - Private methods

- (void)sendLocalNotificationForMessage:(NSString *)message
{
    UILocalNotification *localNotification = [UILocalNotification new];
    localNotification.alertBody = message;
    localNotification.fireDate = [NSDate date];
    localNotification.soundName = UILocalNotificationDefaultSoundName;
    [[UIApplication sharedApplication] scheduleLocalNotification:localNotification];
}

@end

実行すると、iBeacon圏内に入ったとき(家に帰ったとき)と圏外に出たとき(家から出たとき)にやさしいメッセージを投げてくれます・・・

IMG_0416

Twitterで、 localNotification.soundName をDefaultでなく、お気に入りのものに変更するとさらに萌えるとコメントをいただきましたが、そこまではできていません。あと、通知センターへのメッセージでなく、一時的にロック画面を「行ってらっしゃい用・お帰りなさい用」に書き換えることができるとよいなぁと思ったのですが、私のテクではAPIが分からず断念しています。

SBDBT/SBBLEとiPhoneでBLE通信を行う

以前、「iPhoneからmbedをBluetooth LE (BTLE)で制御する」という記事を書いたのですが、今回はその続編です。前回は、RunningElectronicsさんのSBDBTで使われているファームウェア(btstack)をmbedに移植して、iPhoneから、mbedにつないだLEDをBLEで制御できるようにしました。mbedのGPIOへの出力はうまく行ったのですが、GPIOの読み取りをNotificationベースでリアルタイムに飛ばす処理はどうしても動かせませんでした。

最新ファームではNotificationも使えると教えていただき、この際なので、SBDBT本体を購入して試してみました。

 

出荷時ファームウェアの動作確認

出荷時はBluetooth SPPプロファイルで動くファームウェアが搭載されています。ですので、シリアルデータをBluetoothでPCなどに飛ばすことができます。試験では、RaspBerry Piのシリアル出力をMacに飛ばす実験を行いました。無線による遅延もなく、軽快にコンソール操作ができました。

 

IMG_0379

 

SBDBT-SPP_Mac

 

SBBLEファームの使用

PICkitを使って、ファームをBLE用のSBBLEに書き換えます。版数は、”sbble_131224.hex”を使用しています。

この状態で、SBDBTにLEDとSWをつないで、Micono UtilitiesさんのiOSアプリを入れれば、LEDの制御・SWの読み取り(Notificationベース)が簡単にできてしまいます。ここまで環境が整備されていると、もう、自分でやることもないくらいです。

 

IMG_0015

 

せっかく、iOS Developperの更新もしたことだし、気を取り直して、自作の簡単なiOSアプリを作ってみました。SBBLEはATT/GATTの割り当てがKonashiと同一にしてあるため、KonashiのSDKが使えます。CoreBluetoothのAPIをゴリゴリたたくより簡単で、Arduinoやmbedライクなスタイルでプログラミングが出来そうなので、Konashi SDKを使いました。iOSは7.1で、XCodeは5.1を使っています。作成したアプリのデモ画像を以下に示します。

2015/1/26更新:Konashi SDKが3.0にバージョンアップしていたので、3.0に対応しました。また、XCodeは6.1.1 iOS 8.1.2を使用しています。

 

ソースは以下の通りです。

//
//  ViewController.h
//  SBBLE
//
//  Created by Todotani on 2014/03/25.
//  Copyright (c) 2014年 Todotani. All rights reserved.
//

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController

@property (weak, nonatomic) IBOutlet UIButton *finedButton;
@property (weak, nonatomic) IBOutlet UIButton *disconnectButton;
@property (weak, nonatomic) IBOutlet UISwitch *ledSwitch;
@property (weak, nonatomic) IBOutlet UILabel *switchLable;

@end
//
//  ViewController.m
//  SBBLE
//
//  Created by Todotani on 2014/03/25.
//  Update to Konashi SDK 3.0 on 2015/1/25
//  Copyright (c) 2014年 Todotani. All rights reserved.
//

#import "ViewController.h"
#import "Konashi.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    [Konashi initialize];
    [Konashi addObserver:self selector:@selector(ready) name:KonashiEventConnectedNotification];
    [Konashi addObserver:self selector:@selector(input) name:KonashiEventDigitalIODidUpdateNotification];
    [Konashi addObserver:self selector:@selector(disconnected) name:KonashiEventDisconnectedNotification];
    self.finedButton.backgroundColor = [UIColor grayColor];
    self.switchLable.backgroundColor = [UIColor grayColor];
}


- (IBAction)find:(id)sender {
    [Konashi find];
}


- (IBAction)disconnect:(id)sender {
    [Konashi disconnect];
    self.finedButton.backgroundColor = [UIColor grayColor];
}


- (IBAction)switchChanged:(id)sender {
    [self ledControl];
}


- (void)ready
{
    [Konashi pinMode:KonashiDigitalIO1 mode:KonashiPinModeInput];
    [Konashi pinMode:KonashiDigitalIO2 mode:KonashiPinModeOutput];
    [Konashi pinPullup:KonashiDigitalIO1 mode:KonashiPinModePullup];
    [self ledControl];
    self.finedButton.backgroundColor = [UIColor greenColor];
}


- (void)input
{
    if ([Konashi digitalRead:KonashiDigitalIO1] == KonashiLevelLow) {
        self.switchLable.backgroundColor = [UIColor redColor];
    } else {
        self.switchLable.backgroundColor = [UIColor grayColor];
    }
}

- (void)disconnected
{
    self.finedButton.backgroundColor = [UIColor grayColor];
}


- (void)ledControl
{
    if (self.ledSwitch.on == YES) {
        [Konashi digitalWrite:KonashiDigitalIO2 value:KonashiLevelHigh];
    } else {
        [Konashi digitalWrite:KonashiDigitalIO2 value:KonashiLevelLow];
    }
}


- (BOOL)shouldAutorotate
{
    return NO; // YES:自動回転する NO:自動回転しない
}

@end
終わりに

Konashi SDKを使ったおかげで、非常に簡潔なプログラムとなりました。Konashiと比べてSBDBTは価格が安くできることはほぼ同じなので大変お得感があります。(PICkitが必要ですが、こちらは他の用途にも使いまわせますし・・)。最新ファームを使うことで、懸案だったNotification送信も簡単にできてしまい(というか、SBDBT/SBBLEのファームは何も触っていない)、非常に充実した環境だと思いました。

Bluetooth LE (BTLE)のAdvertisement packet format

btstackのコードを追いかけていると、BTLEペリフェラルがadvertisementを行う際のパケットを規定している配列があります。具体的には、こんなのです:

const uint8_t adv_data[31]="\x02\x01\x05" "\x05\x09mbed" "\x03\x02\xf0\xff";

なんとなく、名前やUUIDを定義していると分かるのですが、この値をいじったときに、iOSのCoreBluetoothでスキャンできなくなるなどの動きになったため、どういう構造になっているのかを知りたいと思っていました。Google先生に聞いてもうまくヒットするコンテンツがなかったのですが、結局本家のBluetooth 4.0 Specificationドキュメントで答えを見つけました。2000ページ以上あるドキュメントなので、目次くらいしかみていませんが、Volume-3 (Core System Package), Part-C (Generic Access Profile), Section-11 “ADVERTISING AND SCAN RESPONSE DATA FORMAT”に必要な情報を見つけました。

小ネタですが、BTLEのadvertisement packet formatということで以下に記載します。


Packet Format

Bluetooth Specificationからの抜粋です。

image

  • Advertisement dataは31byteで構成されます
  • AD Structureと呼ばれる情報要素が31byteのpacketに詰め込まれます
  • 個々のAD Structureは、Length – AD Type – AD Dataで構成されます
  • 未使用部分は0で埋める

AD Type値の意味が分かれば、個々のAD Structureが何をしているのかが分かります


AD Type値の割り当て

AD TypeはAssigned Numberという別のドキュメントで定義しています(デバイス種別の追加などで新規割り当て値が増えていくため、規格文書本体とは分けて管理しているようです)。Assigned Numberを公開しているWebサイトはこちらで、そこから、Generic Access Profileに飛ぶと、目的の情報が出てきます。主な情報を記載すると:

名称
0x01 Flag
0x02 Incomplete List of 16-bit Service UUIDs
(more 16-bit UUIDs available)
0x03 Complete List of 16-bit Service Class UUIDs
0x09 Complete Local Name
0x10 Device ID


Flagは各bitが意味を持っており、以下の定義になっています:

bit位置 意味
0 LE Limited Discoverable Mode
1 LE General Discoverable Mode
2 BR/EDR Not Supported
3 Simultaneous LE and BR/EDR capable (Controller)
4 Simultaneous LE and BR/EDR capable (Host)
5..7 Reserved

btstackでは0x05 = 0101bですので、「LE Limited Discoverable Mode & BR/EDR Not Supported」をadvertiseすることになります。


最終的なAdvertise Data

btstackのケースでは、以下のAD Structureをadvertiseしています(要素と記載した部分は、advertise dataには含まれません):

要素 Length Type Data
AD1 2 1 (flag) 0x05: LE Limited Discoverable & BR/EDR Not Supported
AD2 5 9 (Complete Local Name) mbed
AD3 3 2 (Incomplete List of 16-bit Service UUID) 0xFFF0 (配列上では、上位下位バイトを逆に並べる)

UUIDのType値を2 (Incomplete)としているのは、advertiseでは0xFFF0のみを広報しますが、GATT profileとして0x1800, 0x1801も別途通知できるようにしているためだと思います。


おわりに

AdvertiseにService UUIDを含めないことも出来るのですが、その場合、iOS CoreBluetoothのCBCentralManagerクラスインスタンスに対してscanForPeripheralsWithServices: メッセージを送った際に、引数としてUUIDを指定するとペリフェラルを検出してくれません(nil指定にする必要あり)。UUIDをadvertiseしていなので当たり前ですが、最初はbtstackが何をadvertiseしているのかを分かっていなかっため悩みました。Twitterで呟いたら、ランニングエレクトロニクスさん(@sibu2)にService UUIDを追加するように教えていただいた次第でした。


参考資料

iPhoneからmbedをBluetooth LE (BTLE)で制御する

久しぶりの更新です。ずっと弾切れでしたが、最近Bluetooth LE (BTLE)を使ったiPhone (iOSデバイス)とマイコンボード間の通信に興味が湧き、mbedとの接続実験を行いました。

以前から、スマホとmbedやArduinoをつないで連携できるとよいなと思っていました。どうせならUSBなどの有線でなく、無線でつなぎたい、あとAndroidよりiOS派の自分としてはiOSとつなぎたい、というのが目標でしたが、皆様ご存知の通り以下の制約で挫折していました:

  • 以前のiOS (4.x以前)ではBluetoothのSPPが使えない
  • xBee WiFiを使って、TCP/IPのソケット通信でつなぐ方法はありそう。でも、APがないとダメ。

で、iOS5とiPhone 4SからBTLEがサポートされ、BTのプロファイル縛りから解放されていることをつい最近知りました。それで、色々と資料をあさり、iPhoneからmbedのLEDをon/offするサンプルを作ってみました。BTLEのGATT/ATTなど、プロトコルの仕組みはまだよく分かっていないのですが、動かしならが確かめて行きたいと思います。


素材

iOS側はiOS5からサポートされたCoreBluetooth Frameworkを使えばOKです。

マイコン側も色んな選択肢がありそうです:

  • ユカイ工学さんのkonasi :  BluetoothモジュールにGPIO/PWM/ADCなどを組み込んで、iOSから制御できる。Bluetoothのサービス・キャラクタリスティック操作など下回りを意識させないiOS用ライブラリも提供されており、すぐに使えそう。ただ少々お高いのと、執筆現在は品切れ中・・勉強がてら、CoreBluetoothやBTLEを意識する低レイヤーなところから始めても悪くない
  • ランニングエレクトロニクスさんのSBDBT : BTLEとシリアルインタフェースのブリッジとして動き、価格がお手頃。mbedやArduinoなどのマイコンボートと通信するためにはシリアルポート経由でコマンドを送受信するようなインタフェースを作る必要がありそう。モジュールが小型なので、ロボットの無線操縦なんかに向いているのかしら
  • RedBearLabさんのArduino BLE shield : Arduino用のライブラリやiOS用のAppもあり、お手軽に使えそう。お値段も手ごろですが、日本の技適がないため法的に使えません。
  • btstack : 組み込み向けのBluetooth stack。もともとはSPPをサポートしていたが、BTLEにも対応。HCI (Host-Controller Interface)から上の層を実装しており、下位層であるLL (Link Layer)/PHYは自分で持ってくる必要がある。下位層はBluetoothドングルを使うことを想定しているので、USB周りのドライバを用意する必要がある。

今回は、mbedを直接iOSから制御できるようにしようということで、btstackを使いました。問題はUSBのドライバをどうするかです。SBDBTもbtstackを使っていますが、PIC用なのでそのままでは使えません。一方、mbedではSPP版のbtstackをNorimasa Okamotoさんがmbedに移植されていました。

実装(移植)の方針として、OkamotoさんのSPP版btstackをベースに、上位プロトコル部分をBTLE (GATT/ATT)に差し替えました。mbedのリソース(現状はLED1だけですが・・)をBTLEのサービスやキャラクタリスティックとして見せる部分の処理は、ランエレさんのSBDBT用コード(ble-server.c)を参考にさせていただきました。


実装

mbed側のコードは、mbed.orgにポストしてあります。SBDBT同様に以下の動作を行います:

  • サービス(UUID: 0xFFF0)配下に3つのcharacteristicsがありcharacteristic 0xFFF2に00 or 01をライトすることでLED1のon/offを制御できます
  • LED3はbtstackが初期化されアドバタイズを始めた時点で点灯、マスターと接続が完了すると点滅します
  • LED2はアトリビュートのリードで点灯、ライトで消灯します。今回のiOS Appからの制御ではライトのみなので、消えっぱなしです

iOS側のコードは、以下の通りです(iOS 6.1 & Xcode 4.6を使用)。チュートリアルなどを参考にして書いてみました:

//
//  ViewController.h
//  mbed Control
//
//  Created by todotani on 2013/02/11.
//  Copyright (c) todotani. All rights reserved.
//

#import <UIKit/UIKit.h>
#import <CoreBluetooth/CoreBluetooth.h>

@interface ViewController : UIViewController <CBCentralManagerDelegate, CBPeripheralDelegate>

@property (weak, nonatomic) IBOutlet UISwitch *led1Sw;
@property (weak, nonatomic) IBOutlet UILabel *statusLabel;
@property (weak, nonatomic) IBOutlet UISegmentedControl *connectButton;

@end

----

//
//  ViewController.m
//  mbed Control
//
//  Created by todotani on 2013/02/11.
//  Copyright (c) todotani. All rights reserved.
//

#import "ViewController.h"

#define CONECTED     0
#define DISCONNECTED 1

@interface ViewController ()

@property (strong) CBCentralManager *manager;
@property (strong) CBPeripheral *peripheral;
@property (strong) CBCharacteristic *led1Characteristic;

- (void) updateLed1;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.manager = [[CBCentralManager alloc] initWithDelegate:self queue:nil];
}


#pragma mark CoreBuletooth delegate

- (void)centralManagerDidUpdateState:(CBCentralManager *)central{
    NSLog(@"State Update:%d",[self.manager state]);
}

- (void) centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)aPeripheral
      advertisementData:(NSDictionary *)advertisementData
                   RSSI:(NSNumber *)RSSI
{
    self.statusLabel.text = [NSString stringWithFormat:@"Found(RSSI %@)", RSSI];
    [central stopScan];
    self.peripheral = aPeripheral;
    [central connectPeripheral:self.peripheral options:nil];
}

- (void) centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral
                  error:(NSError *)error
{
    NSLog(@"Failed:%@",error);
}

- (void) centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)aPeripheral
{
    NSLog(@"Connected:%@",aPeripheral.UUID);
    self.statusLabel.text = @"Connected";
    [aPeripheral setDelegate:self];
    [aPeripheral discoverServices:@[[CBUUID UUIDWithString:@"FFF0"]]];
}

- (void) centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral
                  error:(NSError *)error
{
    NSLog(@"Disconnected");
    self.statusLabel.text = @"Disconnected";
    self.connectButton.selectedSegmentIndex = DISCONNECTED;
    [self.manager stopScan];
}


// mbed Sericeを検出
- (void) peripheral:(CBPeripheral *)aPeripheral didDiscoverServices:(NSError *)error
{
    for (CBService *aService in aPeripheral.services) {
        NSLog(@"Service UUID:%@", aService.UUID);
        if ([aService.UUID isEqual:[CBUUID UUIDWithString:@"FFF0"]]) {
            [aPeripheral discoverCharacteristics:@[[CBUUID UUIDWithString:@"FFF2"]] forService:aService];
        }
    }
}

// LED1制御用characteristicsを検出
- (void) peripheral:(CBPeripheral *)aPeripheral didDiscoverCharacteristicsForService:(CBService *)service
              error:(NSError *)error
{
    for (CBCharacteristic *aChar in service.characteristics){
        NSLog(@"Characteristic UUID:%@",aChar.UUID);
        if ([aChar.UUID isEqual:[CBUUID UUIDWithString:@"FFF2"]]) {
            self.led1Characteristic = aChar;
            [self updateLed1];
        }
    }
}


#pragma mark ActionMethods

- (IBAction)changeConnection:(id)sender
{
    UISegmentedControl *mySegmented = sender;
    switch (mySegmented.selectedSegmentIndex) {
        case 0:
            [self.manager scanForPeripheralsWithServices: @[[CBUUID UUIDWithString:@"FFF0"]]
                                              options:@{ CBCentralManagerScanOptionAllowDuplicatesKey:@YES }];
            self.statusLabel.text = @"Scanning";
            break;
        case 1:
            if ([self.peripheral isConnected]) {
                [self.manager cancelPeripheralConnection:self.peripheral];
            } else {
                [self.manager stopScan];
                self.statusLabel.text = @"Disconnected";
            }
            break;
        default:
            break;
    }
}


- (IBAction)changeLed1:(id)sender
{
    if ([self.peripheral isConnected]) {
        [self updateLed1];
    }    
}


#pragma mark Private methods

- (void) updateLed1
{
    const unsigned char onData[]  = {0x01};
    const unsigned char offData[] = {0x00};
    
    if ([self.led1Sw isOn]) {
        [self.peripheral writeValue:[NSData dataWithBytes:onData length:1] forCharacteristic:self.led1Characteristic
                               type:CBCharacteristicWriteWithoutResponse];
    } else {
        [self.peripheral writeValue:[NSData dataWithBytes:offData length:1] forCharacteristic:self.led1Characteristic
                               type:CBCharacteristicWriteWithoutResponse];
    }
}

@end

動作の概要は以下です:

  • 105行目:Connectボタンが押されると、CBCentralManagerクラスのscanForPeripheralsWithServicesメソッドを呼び出してBTLEアドバタイズのスキャンを開始。この際、mbedの制御用に定義している、サービス UUID 0xFFF0を検索対象として指定(Arrayに複数の検索対象UUIDをセットできるが、このサンプルでは0xFFF0のみを指定)
  • 39行目:ペリフェラルが見つかると、centralManager:didDiscoverPeripheral: デリゲートがコールバックされます。ここでは何も考えずに、見つかったペリフェラルに接続要求を発行しています。スキャンを行う際に、mbedのサービスを指定していますので、mbed以外と繋がることはないですが、どのmbedかまでは区別していません。接続先をもっと厳密に制御したい場合は、認証などのセキュリティー機能を使うのでしょうか(このあたりはよく分かっていません)
  • 55行目:ペリフェラルとの接続が完了すると、centralManager:didConnectPeripheral:デリゲートがコールバックされます。ここで、見つかったペリフェラルのインスタンスに対して、discoverServicesメッセージを送って、サービスを検索します。引数として、UUID 0xFFF0を指定することで、mbed制御のサービスのみを検出するようにしています(引数をnilにすると、GATT基本サービスの1800や1801が見つかった際にもコールバックされるのでフィルターしておきます)
  • 74行目:サービスが見つかると、peripheral:didDiscoverServices:デリゲートがコールバックされます。今度は、ペリフェラルクラスに対して、discoverCharacteristicsメッセージを送ってキャラクタリスティックの検索を行います。サービス0xFFF0には3つのキャラクタリスティックが定義されていますが、LED1の制御に使うのは0xFFF2のみのため検索条件を指定します。
  • 85行目:キャラクタリスティックが見つかると、peripheral:didDiscoverCharacteristicsForService:デリゲートがコールバックされます。discoverCharacteristicsでフィルター条件をかけているつもりなのですが、3回コールバックされるため、処理対象とする0xFFF2を判定しています。これで、LED1の制御に必要なキャラクタリスティックがViewControllerクラスのプロパティとして設定されました
  • 133行目:スイッチの状態が変化すると、LED1制御用のキャラクタリスティックにon/offに該当する値をライトします

動作中の写真はこんな感じです:

IMG_0123

IMG_0984

 

今後の予定

今回は書き込みのみですので、mbed側のGPIO状態の読み取りを実装するつもり。


参考資料

2023年6月
        1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30  
無料ブログはココログ