2015年5月11日月曜日

BluetoothSMARTデバイスをmbed で開発する(5)

はじめに

お待たせしました、昨年夏に連載していました「BluetoothSMARTデバイスをmbed で開発する(1)-(4)」の続編になります。

2014年の後半は mbed のnRF51822 周りの実装が安定せず掲載を中断しておりましたが、ライブラリを更新してもビルドが失敗するようなこともなくなってきましたので、掲載を再開とさせて頂きたいと思います。
今回からは Android アプリ開発に焦点を当てて、Andorid 5.0 以降で変更となるBLE関連のAPIについても掲載していきます。

関連記事
BluetoothSMARTデバイスをmbed で開発する(1)
BluetoothSMARTデバイスをmbed で開発する(2)
BluetoothSMARTデバイスをmbed で開発する(4)

BluetoothSMARTデバイスをmbed で開発する(6)
BluetoothSMARTデバイスをmbed で開発する(7)
BluetoothSMARTデバイスをmbed で開発する(8)


ARMは、ARM Limited(またはその子会社)のEUまたはその他の国における登録商標です。mbedは、ARM Limited(またはその子会社)のEUまたはその他の国における商標です。All rights reserved.

注意事項 - 本記事に掲載されているソースコードについて

2015年の後半に次世代 mbed である mbed OS がリリースされるというアナウンスが出ています。
mbed OSでは新たに Bluetooth スタックのサポートが追加され API が大幅に変更されることが予測されます。 mbed OS上では本記事の内容が利用できなくなる可能性がありますので予めご了承ください。

お知らせ

前回の記事BluetoothSMARTデバイスをmbed で開発する(4)に掲載されているソースコードが昔のAPI のままであったため修正を行いました。
修正箇所は次の3ヵ所になります。

1) 接続コールバック関数(2015/4/30以降)
<旧> void ConnectionCallback(void
<新> void ConnectionCallback(Gap::Handle_t handle, Gap::addr_type_t type, const Gap::address_t addr, 
 Gap::addr_type_t addr_type_townAddrType, const Gap::address_t ownAddr,
 const Gap::ConnectionParams_t *params)

2) 切断コールバック関数
<旧> void DisconnectionCallback(void)
<新> void DisconnectionCallback(Gap::Handle_t handle, Gap::DisconnectionReason_t reason)

3) GATTデータ更新API
<旧> ble.updateCharacteristicValue(battLevel.getHandle(),
<新> ble.updateCharacteristicValue(battLevel.getValueAttribute().getHandle(),



mbed  BLE & Android プログラミング -- GATT R/W

今回はBLEプログラミング基礎編としてシンプルな機能のBLEデバイスをmbedで作成し Andorid OSでアクセスする事例について紹介いたします。




BLEデバイス内にある Characteristicに対してスマートフォンから書き込みを行うと液晶にデータ表示し、読み出しのときは前回書き込まれた値を読み取りスマートフォン側で表示します。


BLEデバイス基板




今回は写真のようにnRF51822ブレイクアウト基板にI2C接続の液晶とLEDを2個付けた簡単な基板を使います。

BLEデバイス側のmbed プログラミング

今回作成したコードをサンプルとして掲載しました。
mbed IDEでプロジェクトを作成する方法については前回の記事を参考として下さい。
また外部ライブラリにTextLCDを使用していますので追加でインポートを行っています。

サンプルコード


プログラムの構造について

・読み書き可能なCharacteristicを1つ生成しServiceに登録

今回はオリジナル機能BLEデバイスになりますので128bitUUIDを使用することにします。
使用する128bitUUIDはuuidgenで生成しています。

4d9237c0-bd5b-4593-ad55-d8f595cfe2ea <Service UUID>
e5c1cf6e-e057-4008-9821-17711024e885 <Characteristic UUID>

mbedにおける BLE GATT Serviceの登録は次のように行います。
まず UUIDをバイト列として作成し、GattCharacteristic構造体に登録、次にGattCharacteristicをGattService構造体に登録します。

static const uint8_t UUID_BRIL_SERVICE[]    = {0x4d,0x92,0x37,0xc0,0xbd,0x5b,0x45,0x93,0xad,0x55,0xd8,0xf5,0x95,0xcf,0xe2,0xea};
static const uint8_t UUID_CHAR_DATA[]       = {0xe5,0xc1,0xcf,0x6e,0xe0,0x57,0x40,0x08,0x98,0x21,0x17,0x71,0x10,0x24,0xe8,0x85};

uint8_t gRwData[CHARACTERISTIC_LEN] = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
GattCharacteristic  gDataCharacteristic (UUID_CHAR_DATA, gRwData, sizeof(gRwData), sizeof(gRwData),
        GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_READ
      | GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_WRITE);

GattCharacteristic *RwDataChars[] = {&gDataCharacteristic};
GattService gBrilService = GattService(UUID_BRIL_SERVICE, RwDataChars, sizeof(RwDataChars) / sizeof(GattCharacteristic *));

・BLEデバイスを外部から検出できるようにする

BLEデバイス起動時に Advertisingパラメータに対してGattService構造体を登録しAdvertisingを開始します

/* setup advertising */
ble.accumulateAdvertisingPayload(
       
GapAdvertisingData::LE_GENERAL_DISCOVERABLE |  GapAdvertisingData::BREDR_NOT_SUPPORTED);

ble.setAdvertisingType(GapAdvertisingParams::ADV_CONNECTABLE_UNDIRECTED);
ble.accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LIST_128BIT_SERVICE_IDS,                             
       (const uint8_t *)UUID_BRIL_SERVICE, sizeof(UUID_BRIL_SERVICE));

ble.accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LOCAL_NAME,
       (const uint8_t *)DEVICE_NAME, strlen(DEVICE_NAME));

ble.setAdvertisingInterval(800); /* 500ms; in multiples of 0.625ms. */
ble.setAdvertisingTimeout(0); /* 0 is disable the advertising timeout. */
ble.startAdvertising();
ble.addService(gBrilService);

・BLE接続/切断時の処理

BLE接続/切断時の処理はコールバック関数に実装します。
今回は液晶への状態表示とLED点灯/消灯のみを行っています。
他に処理を入れたい場合はこの関数内に記述するようにしてください。
※ここで記載されているコードは2015/4/30以降のBLE_APIで有効です

void ConnectionCallback(Gap::Handle_t handle, Gap::addr_type_t type, const Gap::address_t addr,
                        Gap::addr_type_t addr_type_townAddrType, const Gap::address_t ownAddr,

                        
const Gap::ConnectionParams_t *params)
{
    DEBUG("Connected!\n\r");
    led_grn = 1;
    lcd.locate(0,1);
    lcd.printf("Connect ");
}

void DisconnectionCallback(Gap::Handle_t handle, Gap::DisconnectionReason_t reason)
{
    DEBUG("Disconnected!\n\r");
    led_grn = 0;
    lcd.locate(0,1);
    lcd.printf("DisConn ");
    ble.startAdvertising();
}


コールバック関数の登録はBLEデバイス起動時に行います。

ble.init();

/* Setup The event handlers */
ble.onConnection(ConnectionCallback);
ble.onDisconnection(DisconnectionCallback);


・Characteristicに書き込み時の処理

Characteristicに書き込まれた時は最初の2バイトの数値を液晶に表示します。
まず書き込み処理のコールバック関数を作成します。書き込みデータはparams->dataに入っていますので、最初の2バイトを16進数で液晶に出力するようにしました。

void DataWrittenCallback(const GattCharacteristicWriteCBParams *params)
{
    DEBUG("DataWritten\n\r");
    lcd.locate(0,1);
    lcd.printf("Wr:");
    lcd.printf("%02x%02x",params->data[0],params->data[1]);
}

コールバック関数の登録はBLE接続/切断と同様にBLEデバイス起動時に行います

ble.init();
.....
ble.onDataWritten(DataWrittenCallback);


・Characteristic読み出し時の処理

読み出し時は Characteristicに書き込まれた値をそのまま読み出すようになっていますので
処理を作成する必要はありません。もし BLEデバイス側で読み出しデータを書き換えたいという場合は、前回の記事に記載されていたble.updateCharacteristicValue()を使ってデータ更新を行えば実現することができます。

・mbed デバッグ方法
ソースコードの最初のほうにある定義を1に変更するとデバッグメッセージがシリアルポートに出るようになります。

#define NEED_CONSOLE_OUTPUT 0 /* Set this if you need debug messages on the console;
                               * it will have an impact on code-size and power consumption. */


nRF51822とmbed Interfaceが一体化したボードの場合は mbed シリアルドライバを入れればシリアルポートと接続することができます。また今回のようにnRF51ブレイクアウト基板を用いる場合はTXピンからシリアル信号を取り出します。
シリアルポートの接続条件は 9600bps Non-Parity 1-StopBitsです。

Androidアプリの書き方

次はAndroid側プログラミングです。本記事では Android 4.4(KitKat API level 19)での実装と、Android 5.x(Lolipop API level 21) におけるAPI変更について紹介いたします。

今回の記事では既定の名前が付いたBLEデバイスを検索し、Characteristicに対してデータの読み出しと書き込みを行うだけのシンプル機能のプログラムを題材にしました。




1.アプリ起動後、"Conect"ボタンを押すとデバイスとの接続が行われる
2."Read"ボタンを押すとデバイスのCharacteristic からデータ読み出し
 読み出したデータはバイト配列のまま16進表示
3."Write"ボタンを押すと左側のエディットボックスに入力した数値がCharacteristicに書き込まれる
 入力は"0x"なしの16進数でLittle-Endianでデータ書き込みが行われる
4."Disconnect"ボタンでデバイスとの接続切断が行われる


ソースコード(Android 4.4版)(Android 5.x版)を公開しています。
Eclipse-ADTのプロジェクト構成となっていますので適度にインポートして下さい。

・BLEデバイスの検索

BLEデバイスが接続待ちの時に出力しているAdvertisingパケットを受信します。なお Andoridのバージョン(API Level)によって使用できるAPIの差異が出てきました。
Android5.xが主流になっても API Level 19でアプリを書き続ければ動作させることが可能ですが、API Level 21以上で書かなければならなくなったときのことも考えて早いうちに両方のAPIに対応しておくことをお奨めします。

<API Level-19>
AndroidでBeaconを触ったことがある方ならお馴染みのコードですね。
StartLeScan()を実行してデバイス検索のコールバック関数呼び出しを待ちます。デバイスが見つかると見つけた回数だけコールバック関数の呼び出しが行われます。もしスキャンの必要がなくなったときは StopLeScan()でデバイス検索を止めるようにしましょう。

BluetoothManager mBluetoothManager = (BluetoothManager)getSystemService(BLUETOOTH_SERVICE);
BluetoothAdapter mBluetoothAdapter = mBluetoothManager.getAdapter();

/** BLE機器を検索する */
private void connect() {
 mHandler.postDelayed(new Runnable() {
  @Override
  public void run() {
   // SCAN_PERIODで指定した時間内にデバイスが見つからなかった
   mBluetoothAdapter.stopLeScan(mLeScanCallback);
  }
 }, SCAN_PERIOD);

// mBluetoothAdapter.stopLeScan(mLeScanCallback);
 mBluetoothAdapter.startLeScan(mLeScanCallback);
}

private BluetoothAdapter.LeScanCallback mLeScanCallback = new BluetoothAdapter.LeScanCallback() {
 @Override
 public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
  Log.d(TAG, "device found: " + device.getName() + " :" + device.getUuids());
  if (DEVICE_NAME.equals(device.getName())) {
   // デバイスを見つけたのでスキャンを停止する
   mBluetoothAdapter.stopLeScan(this);

   // 以下接続処理
  }
 }
};


<API Level-21> Android 5.0以降
基本構造はAPI Level-19同じですが利用できるAPIが変わりました。
それまで直接 StartLeScan()を呼び出していたものを BluetoothLeScannerクラスのStartScan()を使う形になっています。
またコールバック関数内で多くの情報を取得できるようになりました。特にUUID判定が簡単に出来るようになったのが大きい進歩だと思います。

BluetoothManager mBluetoothManager = (BluetoothManager)getSystemService(BLUETOOTH_SERVICE);
BluetoothAdapter mBluetoothAdapter = mBluetoothManager.getAdapter();
BluetoothLeScanner mBleScanner = mBluetoothAdapter.getBluetoothLeScanner();

/** BLE機器を検索する */
private void connect() {
 mHandler.postDelayed(new Runnable() {
  @Override
  public void run() {
   // SCAN_PERIODで指定した時間内にデバイスが見つからなかった
   mBleScanner.stopScan(mScanCallback);
   }
  }, SCAN_PERIOD);

// mBleScanner.stopScan(mScanCallback);
 mBleScanner.startScan(mScanCallback);
}

private ScanCallback mScanCallback = new ScanCallback() {
 @Override
 public void onScanResult(int callbackType, android.bluetooth.le.ScanResult result) {
         Log.i(TAG, "onScanResult()");
         Log.i(TAG, "DeviceName:" + result.getDevice().getName());
         Log.i(TAG, "DeviceAddr:" + result.getDevice().getAddress());
         Log.i(TAG, "RSSI:" + result.getRssi());
         Log.i(TAG, "UUID:" + result.getScanRecord().getServiceUuids());

         if (DEVICE_NAME.equals(result.getDevice().getName())) {
   // デバイスを見つけたのでスキャンを停止する
          mBleScanner.stopScan(mScanCallback);

   // 以下接続処理
  }
 };

 public void onScanFailed(int errorCode) {
         Log.e(TAG, "onScanFailed() errorCode=" + errorCode);
 };
};


・GATT Serviceを検索する

デバイスの検索で目的のデバイスを見つけたらBLEデバイスに対してGATT接続を行い、デバイス内にあるサービス情報を取得します。

<API Level-19>
先ほどのコールバック関数onLeScan()内でデバイス判定を行ったあと、そのまま関数内にある変数を使ってconnectGatt()を実行します

private BluetoothAdapter.LeScanCallback mLeScanCallback = new BluetoothAdapter.LeScanCallback() {
 @Override
 public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
  Log.d(TAG, "device found: " + device.getName() + " :" + device.getUuids());
  if (DEVICE_NAME.equals(device.getName())) {
   // デバイスを見つけたのでスキャンを停止する
   mBluetoothAdapter.stopLeScan(this);

   // GATT接続を試みる
   mBluetoothGatt = device.connectGatt(getApplicationContext(), false, mBluetoothGattCallback);
  }
 }
};


<API Level-21> Android 5.0以降
先ほどのコールバック関数onScanResult()内でデバイス判定を行ったあとGATT接続を行います。なお ScanCallbackではBluetoothDevice構造体が引数に入っていないため、result.getDevice()を用いてconnectGatt()を実行するようにしています。

private ScanCallback mScanCallback = new ScanCallback() {
 @Override
 public void onScanResult(int callbackType, android.bluetooth.le.ScanResult result) {

  ....

         if (DEVICE_NAME.equals(result.getDevice().getName())) {
   // デバイスを見つけたのでスキャンを停止する
         
   mBleScanner.stopScan(mScanCallback);

   // GATT接続を試みる
          mBluetoothGatt = result.getDevice().connectGatt(getApplicationContext(), false, mBluetoothGattCallback);
  }
 };

 public void onScanFailed(int errorCode) {
         Log.e(TAG, "onScanFailed() errorCode=" + errorCode);
 };

};


GATT接続が成功するとコールバック関数onConnectionStateChange()が呼ばれますので、デバイス内のGATTサービス検索を行います。

private final BluetoothGattCallback mBluetoothGattCallback = new BluetoothGattCallback() {
 @Override
 public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
  Log.d(TAG, "onConnectionStateChange: " + status + " -> " + newState);

  if (newState == BluetoothProfile.STATE_CONNECTED) {
   // GATTへ接続成功
   // サービスを検索する
   gatt.discoverServices();

  } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
   // GATT通信から切断された
   mBluetoothGatt = null;
  }
 }

....

}


・GATT Characteristicへのアクセス

デバイス内のGATTサービス検索が成功するとコールバック関数onServicesDiscovered()が呼び出されます。ここで必要とされるCharacteristic情報を取得し、デバイス切断までメンバー変数に保存しておきます。

private final BluetoothGattCallback mBluetoothGattCallback = new BluetoothGattCallback() {

....
 @Override
 public void onServicesDiscovered(BluetoothGatt gatt, int status) {
  Log.d(TAG, "onServicesDiscovered received: " + status);
  if (status == BluetoothGatt.GATT_SUCCESS) {
   BluetoothGattService service = 
    gatt.getService(UUID.fromString(DEVICE_BRIL_SERVICE_UUID));

   if (service == null) {
    // サービスが見つからなかった場合
   } else {
    // サービスを見つけた場合はcharacteristic情報を取得
    mDataCharacteristic =
     service.getCharacteristic(UUID.fromString(DEVICE_RDWT_CHARACTERISTIC_UUID));
    if (mDataCharacteristic == null) {
     // キャラクタリスティックが見つからなかった場合
    else {
     // キャラクタリスティックが見つかったら、ステータスを接続状態に更新する
     mGatt = gatt;
    }
   }
  }
 }
}

・GATT Characteristicからの読み出し
GATT Characteristicへの書き込みwriteCharacteristic()を使います。
まずCharacteristicに対してデータをセットしたのちにwriteCharacteristic()を使ってGATTデータを送信します。
結果はコールバック関数onCharacteristicWrite()にて受け取ります。

private void ReadGatt() {
 if (mDataCharacteristic != null) {
  mGatt.readCharacteristic(mDataCharacteristic);
 }
}

@Override
public void onCharacteristicRead(BluetoothGatt gatt,BluetoothGattCharacteristic characteristic,int status) {
 Log.d(TAG, "onCharacteristicRead: " + status);
 if (status == BluetoothGatt.GATT_SUCCESS) {
  // READ成功
  byte[] read_data = characteristic.getValue();
  // 読み出したデータを処理する
 }
}

・GATT Characteristicへの書き込み
GATT Characteristicへの書き込みwriteCharacteristic()を使います。
まずCharacteristicに対してデータをセットしたのちにwriteCharacteristic()を使ってGATTデータを送信します。
結果はコールバック関数onCharacteristicWrite()にて受け取ります。

private void WriteGatt() {
 if (mDataCharacteristic != null) {
  ....
  byte[] WrtVal = new byte[4];
  // 書き込みデータを準備
  mDataCharacteristic.setValue(WrtVal);
  boolean status = mGatt.writeCharacteristic(mDataCharacteristic);
  if (status == false) {
   Log.e(TAG, "writeCharacteristic failed");
  }
 }
}

@Override
public void  onCharacteristicWrite(BluetoothGatt gatt,BluetoothGattCharacteristic characteristic, int status) {
 Log.d(TAG, "onCharacteristicWrite: " + status);
 switch(status) {
 case BluetoothGatt.GATT_SUCCESS:
  Log.e(TAG, "onCharacteristicWrite: GATT_SUCCESS");
  break;
 case BluetoothGatt.GATT_WRITE_NOT_PERMITTED:
  Log.e(TAG, "onCharacteristicWrite: GATT_WRITE_NOT_PERMITTED");
  break;
 case BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED:
  Log.e(TAG, "onCharacteristicWrite: GATT_REQUEST_NOT_SUPPORTED");
  break;
 case BluetoothGatt.GATT_FAILURE:
  Log.e(TAG, "onCharacteristicWrite: GATT_FAILURE");
  break;
 case BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION:
  break;
 
 case BluetoothGatt.GATT_INSUFFICIENT_ENCRYPTION:
  break;
 case BluetoothGatt.GATT_INVALID_OFFSET:
  break;
 }
}

まとめ

今回はmbedを使ったBLEデバイスと Androidアプリの両面からプログラミング作成方法について紹介してみました。一つ一つの機能をシンプルにして考えてみると、難しいと思われがちなBLEも簡単に見えてくるかと思います。
次回は引き続きGATTデータ読み出し、そして通知の実装について掲載を行う予定です。


関連記事
140 180 Bluetooth , GAP , GATT , mbed , Nordic , nRF51822 , ツール作成

記載されている会社名、および商品名等は、各社の商標または登録商標です。

0 コメント:

コメントを投稿

Related Posts Plugin for WordPress, Blogger...