Bluetooth

2018年06月12日

SwitchBot を ESP32 で遠隔操作してみた

はてなブックマークに登録

SwitchBot と公開 API

物理的なスイッチやボタンを人に代わって操作してくれる BLE デバイス SwitchBot (by Wonderlabs,Inc.が人気です。有線/無線通信や赤外線等によるコントロールに対応していない機器をいわば強引に IoT 方面へ引き込もうという発想が面白いですね。


gigazine.net

技適マークと番号はパッケージに印刷されている

これは同じくクラウドファンディングを経て 2016年に商品化された MicroBot Push (by Naran Inc.: 工事設計認証番号 R208-160099 の後追い製品ではありますが、手元にある両者を比べてみると SwitchBot には後発であることのアドバンテージが随所に活かされているように見受けられます。とりわけ嬉しいのは本体のアームを制御するための API が公開されていることです。

この公約のとおり Wonderlabs,Inc. は Apache License 2.0 のもとに次のリソースを公開しています。

SwitchBot のアーム操作に必要な手順が次のシンプルな内容であることがわかります。

  • 対象とする BLE GATT サービスはユーザ定義の「cba20d00-224d-11e6-9fb8-0002a5d5c51b
  • その配下のキャラクタリスティック「cba20002-224d-11e6-9fb8-0002a5d5c51b」がターゲット
  • 上記キャラクタリスティックへ次の値を書き込むとそれぞれ以下の挙動となる
    • { 0x57, 0x01, 0x00 } : アームを 「倒す+引く」 ("Press")
    • { 0x57, 0x01, 0x01 } : アームを 「倒す」 ("Turn On")
    • { 0x57, 0x01, 0x02 } : アームを 「引く」 ("Turn Off")

以下は以前紹介した手順で採取した GATT 管理下の UUID の一覧です。

onServicesDiscovered: serviceList.size=4

onServicesDiscovered: svc uuid=00001800-0000-1000-8000-00805f9b34fb // Generic Access
onServicesDiscovered: chrlist.size=3
onServicesDiscovered:  chr uuid=00002a00-0000-1000-8000-00805f9b34fb // Device Name
onServicesDiscovered:  desclist.size=0
onServicesDiscovered:  chr uuid=00002a01-0000-1000-8000-00805f9b34fb // Appearance
onServicesDiscovered:  desclist.size=0
onServicesDiscovered:  chr uuid=00002a04-0000-1000-8000-00805f9b34fb // Peripheral Preferred Connection Parameters
onServicesDiscovered:  desclist.size=0

onServicesDiscovered: svc uuid=00001801-0000-1000-8000-00805f9b34fb // Generic Attribute
onServicesDiscovered: chrlist.size=0

onServicesDiscovered: svc uuid=0000fee7-0000-1000-8000-00805f9b34fb // Custom UUID of Tencent Holdings Limited
onServicesDiscovered: chrlist.size=3
onServicesDiscovered:  chr uuid=0000fec8-0000-1000-8000-00805f9b34fb // Custom UUID of Apple, Inc.
onServicesDiscovered:  desclist.size=1
onServicesDiscovered:   desc uuid=00002902-0000-1000-8000-00805f9b34fb // Client Characteristic Configuration Descriptor
onServicesDiscovered:  chr uuid=0000fec7-0000-1000-8000-00805f9b34fb // Custom UUID of Apple, Inc.
onServicesDiscovered:  desclist.size=0
onServicesDiscovered:  chr uuid=0000fec9-0000-1000-8000-00805f9b34fb // Custom UUID of Apple, Inc.
onServicesDiscovered:  desclist.size=0

onServicesDiscovered: svc uuid=cba20d00-224d-11e6-9fb8-0002a5d5c51b // User defined service
onServicesDiscovered: chrlist.size=2
onServicesDiscovered:  chr uuid=cba20003-224d-11e6-9fb8-0002a5d5c51b  // User defined characteristic
onServicesDiscovered:  desclist.size=1
onServicesDiscovered:   desc uuid=00002902-0000-1000-8000-00805f9b34fb // // Client Characteristic Configuration Descriptor
onServicesDiscovered:  chr uuid=cba20002-224d-11e6-9fb8-0002a5d5c51b  // User defined characteristic
onServicesDiscovered:  desclist.size=0

ちなみに、手元の SwithiBot 個体の MAC アドレスは「C0:65:9A:7D:61:E1」でした。

自作の Android アプリで操作してみる

上記の手順にそってまず Android アプリをざっくり書いてみました。以下は動作の様子を収めた動画とソースコードです。

動画:40秒 音量注意

SwitchBot01 - MainActivity.java

/**
 *
 * SwitchBot01
 *
 * SwitchBot を操作する
 *
 * メーカーが公式に公開しているプログラムをベースに作成
 *
 * https://github.com/OpenWonderLabs/python-host/
 *
 */

package jp.klab.SwitchBot01;

import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanRecord;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import com.google.android.gms.appindexing.Action;
import java.util.List;
import java.util.UUID;

public class MainActivity extends AppCompatActivity
    implements Runnable, View.OnClickListener, Handler.Callback {

  private static final String TAG = "SB01";
  private static final String TARGET_ADDR = "C0:65:9A:7D:61:E1"; // 手元の SwitchBot

  private static final int SCAN_MODE = ScanSettings.SCAN_MODE_LOW_LATENCY;

  private static final int MSG_DOSCAN = 100;
  private static final int MSG_FOUNDDEVICE = 110;
  private static final int MSG_STOPSCAN = 120;
  private static final int MSG_GATTCONNECT = 200;
  private static final int MSG_GATTCONNECTED = 210;
  private static final int MSG_GATTDISCONNECT = 300;
  private static final int MSG_GATTDISCONNECTED = 310;
  private static final int MSG_GATTGOTSERVICE = 400;
  private static final int MSG_SW_ON = 500;
  private static final int MSG_SW_OFF = 510;
  private static final int MSG_SW_PRESS = 520;
  private static final int MSG_ERROR = 10;
  private static final int REQ_ENABLE_BT = 0;

  private BluetoothAdapter mBtAdapter = null;
  private BluetoothLeScanner mBtScanner = null;
  private BluetoothGatt mBtGatt = null;
  private BluetoothDevice mBtDevice;
  private Handler mHandler;

  private Context mCtx;
  private ProgressDialog mProgressDlg = null;

  private TextView mTvAddr;
  private TextView mTvRssi;
  private Button mButtonDisconn;
  private Button mButtonConn;
  private Button mButtonTurnOn;
  private Button mButtonTurnOff;
  private Button mButtonPress;

  private BluetoothGattCharacteristic mChUser2 = null;
  private BluetoothGattDescriptor mDescUser1 = null;

  // SwitchBot の提供するサービス・キャラクタリスティック群の UUID より
  private UUID mUuidSvcUser1 = UUID.fromString("cba20d00-224d-11e6-9fb8-0002a5d5c51b");
  private UUID mUuidChUser2  = UUID.fromString("cba20002-224d-11e6-9fb8-0002a5d5c51b");

  // SwitchBot の 3 コマンド
  private byte[] mCmdSwPress = new byte[] {(byte)0x57, (byte)0x01, (byte)0x00};
  private byte[] mCmdSwOn    = new byte[] {(byte)0x57, (byte)0x01, (byte)0x01};
  private byte[] mCmdSwOff   = new byte[] {(byte)0x57, (byte)0x01, (byte)0x02};

  private ScanCallback mScanCallback = new bleScanCallback();
  private BluetoothGattCallback mGattCallback = new bleGattCallback();

  // GATT イベントコールバック
  private class bleGattCallback extends BluetoothGattCallback {
    @Override
    public void onDescriptorWrite(BluetoothGatt gatt,
                    BluetoothGattDescriptor desc,
                    int status) { // writeDescriptor() 結果
      super.onDescriptorWrite(gatt, desc, status);
    }
    @Override
    public void onCharacteristicRead(BluetoothGatt gatt,
                     BluetoothGattCharacteristic ch,
                     int status) { // readCharacteristic() 結果
      super.onCharacteristicRead(gatt, ch, status);
      Log.d(TAG, "onCharacteristicRead: sts=" + status);
    }
    @Override
    public void onCharacteristicWrite(BluetoothGatt gatt,
                      BluetoothGattCharacteristic ch,
                      int status) { // writeCharacteristic 結果
      super.onCharacteristicWrite(gatt, ch, status);
      Log.d(TAG, "onCharacteristicWrite: sts=" + status);
    }

    @Override
    public void onConnectionStateChange(BluetoothGatt gatt, int status,
                      int newState) {
      super.onConnectionStateChange(gatt, status, newState);
      if (newState == BluetoothProfile.STATE_CONNECTED) {  // 接続完了
        mHandler.sendEmptyMessage(MSG_GATTCONNECTED);
      } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { // 切断完了
        mHandler.sendEmptyMessage(MSG_GATTDISCONNECTED);
      }
    }

    @Override
    public void onCharacteristicChanged (BluetoothGatt gatt,
                       BluetoothGattCharacteristic ch) {
      Log.d(TAG, "onCharacteristicChanged");
    }

    @Override
    public void onServicesDiscovered(BluetoothGatt gatt, int status) { // GATT サービス一覧取得完了
      super.onServicesDiscovered(gatt, status);

      // SwitchBot のユーザ定義サービス のユーザ定義キャラクタリスティック 2 のオブジェクトを取得
      BluetoothGattService svc = gatt.getService(mUuidSvcUser1);
      mChUser2 = svc.getCharacteristic(mUuidChUser2);

/**  すべての Services - Characteristics - Descriptors をログへ**
 List<BluetoothGattService> serviceList = gatt.getServices();

 Log.d(TAG, "onServicesDiscovered: serviceList.size=" + serviceList.size());

 for (BluetoothGattService s : serviceList) {
 Log.d(TAG, "onServicesDiscovered: svc uuid=" + s.getUuid().toString());
 List<BluetoothGattCharacteristic> chlist = s.getCharacteristics();
 Log.d(TAG, "onServicesDiscovered: chrlist.size=" + chlist.size());

 for (BluetoothGattCharacteristic c : chlist) {
 UUID uuid = c.getUuid();
 Log.d(TAG, "onServicesDiscovered:  chr uuid=" + uuid.toString());
 List<BluetoothGattDescriptor> dlist = c.getDescriptors();

 Log.d(TAG, "onServicesDiscovered:  desclist.size=" + dlist.size());
 for (BluetoothGattDescriptor d : dlist) {
 Log.d(TAG, "onServicesDiscovered:   desc uuid=" + d.getUuid());
 }
 }
 }
**/
      mHandler.sendEmptyMessage(MSG_GATTGOTSERVICE);
    }
  };

  // SCAN イベントコールバック
  private class bleScanCallback extends ScanCallback {
    @Override
    public void onBatchScanResults(List<ScanResult> results) {
      super.onBatchScanResults(results);
      Log.d(TAG, "onBatchScanResults");
    }
    @Override
    public void onScanResult(int callbackType, ScanResult result) {
      super.onScanResult(callbackType, result);
      int rssi = result.getRssi();
      mBtDevice = result.getDevice();
      ScanRecord rec = result.getScanRecord();
      String addr = mBtDevice.getAddress();
      if (!addr.equals(TARGET_ADDR)) {
        return;
      }
      mTvAddr.setText("ADDRESS\n" + TARGET_ADDR);
      mTvRssi.setText("RSSI\n" + rssi);
      mHandler.sendEmptyMessage(MSG_FOUNDDEVICE);
    }
    @Override
    public void onScanFailed(int errorCode) {
      super.onScanFailed(errorCode);
      Log.e(TAG, "onScanFailed: err=" + errorCode);
    }
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Log.d(TAG, "onCreate");
    mCtx = this;
    setContentView(R.layout.activity_main);
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    mTvAddr = (TextView) findViewById(R.id.tvAddr);
    mTvRssi = (TextView) findViewById(R.id.tvRssi);
    mButtonDisconn = (Button)findViewById(R.id.buttonDisconn);
    mButtonConn = (Button)findViewById(R.id.buttonConn);
    mButtonTurnOn = (Button)findViewById(R.id.buttonTurnOn);
    mButtonTurnOff = (Button)findViewById(R.id.buttonTurnOff);
    mButtonPress = (Button)findViewById(R.id.buttonPress);
    mButtonDisconn.setOnClickListener(this);
    mButtonConn.setOnClickListener(this);
    mButtonTurnOn.setOnClickListener(this);
    mButtonTurnOff.setOnClickListener(this);
    mButtonPress.setOnClickListener(this);
    setButtonsVisibility(false);
    setButtonsEnabled(false);
    setTvColor(Color.LTGRAY);

    mHandler = new Handler(this);

    /***** add for Android 6.0 or later ****/
    // https://developer.android.com/training/permissions/requesting.html
    // https://developer.android.com/topic/libraries/support-library/index.html#backward
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) !=
        PackageManager.PERMISSION_GRANTED) {
      ActivityCompat.requestPermissions(this,
          new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, 1);
    }

    // 端末の Bluetooth アダプタへの参照を取得
    mBtAdapter = ((BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE)).getAdapter();

    if (mBtAdapter == null) {
      // Bluetooth サポートなし
      showDialogMessage(this, "Device does not support Bluetooth.", true); // finish
    } else if (!mBtAdapter.isEnabled()) {
      // Bluetooth 無効状態
      Log.d(TAG, "Bluetooth is not enabled.");
      // 有効化する
      Intent it = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
      startActivityForResult(it, REQ_ENABLE_BT);
    } else {
      mBtScanner = mBtAdapter.getBluetoothLeScanner();
      if (!mBtAdapter.isMultipleAdvertisementSupported()) {
        showDialogMessage(this, "isMultipleAdvertisementSupported NG.", true); // finish
      } else {
        // スキャン開始
        mHandler.sendEmptyMessage(MSG_DOSCAN);
      }
    }
  }

  @Override
  protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    Log.d(TAG, "onActivityResult");
    switch (requestCode) {
      case REQ_ENABLE_BT:
        // Bluetooth 有効化 OK
        if (resultCode == Activity.RESULT_OK) {
          Log.d(TAG, "REQ_ENABLE_BT OK");
          // スキャン開始
          mHandler.sendEmptyMessage(MSG_DOSCAN);
        } else {
          Log.d(TAG, "REQ_ENABLE_BT Failed");
          mHandler.sendEmptyMessage(MSG_ERROR); // finish
        }
        break;
    }
  }

  @Override
  public void onStart() {
    super.onStart();
    Log.d(TAG, "onStart");
    Action viewAction = Action.newAction(
        Action.TYPE_VIEW, // TODO: choose an action type.
        "Main Page", // TODO: Define a title for the content shown.
        // TODO: If you have web page content that matches this app activity's content,
        // make sure this auto-generated web page URL is correct.
        // Otherwise, set the URL to null.
        Uri.parse("http://host/path"),
        // TODO: Make sure this auto-generated app deep link URI is correct.
        Uri.parse("android-app://jp.klab.SwitchBot01/http/host/path")
    );
  }

  @Override
  public void onStop() {
    super.onStop();
    Log.d(TAG, "onStop");
  }

  @Override
  public void onDestroy() {
    super.onDestroy();
    Log.d(TAG, "onDestroy");
    // GATT 接続終了
    if (mBtGatt != null) {
      mBtGatt.disconnect();
      mBtGatt.close();
      mBtGatt = null;
    }
    // スキャン停止
    if (mBtScanner != null) {
      mBtScanner.stopScan(mScanCallback);
      mBtScanner = null;
    }
  }

  @Override
  public void onClick(View v) {
    if (v == (View)mButtonConn) {
      mHandler.sendEmptyMessage(MSG_GATTCONNECT);
    } else if (v == (View)mButtonTurnOn) {
      mHandler.sendEmptyMessage(MSG_SW_ON);
    } else if (v == (View)mButtonTurnOff) {
      mHandler.sendEmptyMessage(MSG_SW_OFF);
    } else if (v == (View)mButtonPress) {
      mHandler.sendEmptyMessage(MSG_SW_PRESS);
    } else  if (v == (View)mButtonDisconn) {
      mHandler.sendEmptyMessage(MSG_GATTDISCONNECT);
    }
      return;
  }

  @Override
  public boolean handleMessage(Message msg) {
    switch (msg.what) {
      case MSG_DOSCAN: // スキャン開始
        Log.d(TAG, "msg: MSG_DOSCAN");
        ScanSettings scanSettings = new ScanSettings.Builder().
                setScanMode(SCAN_MODE).build();
        mBtScanner.startScan(null, scanSettings, mScanCallback);
        break;

      case MSG_STOPSCAN: // スキャン停止
        Log.d(TAG, "msg: MSG_STOPSCAN");
        mBtScanner.stopScan(mScanCallback);

        break;

      case MSG_FOUNDDEVICE: // ペリフェラルのアドバタイズパケットを検出
        Log.d(TAG, "msg: MSG_FOUNDDEVICE");
        mHandler.sendEmptyMessage(MSG_STOPSCAN);
        setTvColor(Color.BLACK);
        setButtonsVisibility(true);
        break;

      case MSG_GATTCONNECT: // デバイスへの接続を開始
        Log.d(TAG, "msg: MSG_GATTCONNECT");
        showProgressMessage(getString(R.string.app_name), "デバイスへ接続中・・・");
        mBtGatt = mBtDevice.connectGatt(mCtx, false, mGattCallback);
        mBtGatt.connect();

        break;

      case MSG_GATTCONNECTED: // デバイスへの接続が完了
        Log.d(TAG, "msg: MSG_GATTCONNECTED");
        setTvColor(Color.LTGRAY);
        // デバイスの GATT サービス一覧の取得へ
        mBtGatt.discoverServices();
        break;

      case MSG_GATTGOTSERVICE: // デバイスの GATT サービス一覧取得完了
        Log.d(TAG, "msg: MSG_GATTGOTSERVICE");
        if (mProgressDlg != null) {
          mProgressDlg.cancel();
          mProgressDlg = null;
        }
        setButtonsEnabled(true);
        break;

      case MSG_GATTDISCONNECT: // デバイスとの切断
        Log.d(TAG, "msg: MSG_GATTDISCONNECT");
        mBtGatt.disconnect();
        break;

      case MSG_GATTDISCONNECTED: // デバイスとの切断完了
        Log.d(TAG, "msg: MSG_GATTDISCONNECTED");
          setButtonsEnabled(false);
          if (mBtGatt != null) {
            mBtGatt.close();
            mBtGatt = null;
          }
          showDialogMessage(mCtx, "デバイスとの接続が切断されました", false);
          mHandler.sendEmptyMessage(MSG_DOSCAN);
        break;

      case MSG_SW_ON: // Turn On
        Log.d(TAG, "msg: MSG_SW_ON");
        mChUser2.setValue(mCmdSwOn);
        mBtGatt.writeCharacteristic(mChUser2);
        break;

      case MSG_SW_OFF: // Turn Off
        Log.d(TAG, "msg: MSG_SW_OFF");
        mChUser2.setValue(mCmdSwOff);
        mBtGatt.writeCharacteristic(mChUser2);
        break;

      case MSG_SW_PRESS: // PRESS
        Log.d(TAG, "msg: MSG_SW_PRESS");
        mChUser2.setValue(mCmdSwPress);
        mBtGatt.writeCharacteristic(mChUser2);
        break;

      case MSG_ERROR:
        showDialogMessage(this, "処理を継続できないため終了します", true);
        break;
    }
    return true;
  }

  @Override
  public void run() {
  }

  // ダイアログメッセージ
  private void showDialogMessage(Context ctx, String msg, final boolean bFinish) {
    new AlertDialog.Builder(ctx).setTitle(R.string.app_name)
        .setMessage(msg)
        .setPositiveButton("OK", new DialogInterface.OnClickListener() {
          public void onClick(DialogInterface dialog, int whichButton) {
            if (bFinish) {
              finish();
            }
          }
        }).show();
  }

  // プログレスメッセージ
  private void showProgressMessage(String title, String msg) {
    if (mProgressDlg != null) {
      return;
    }
    mProgressDlg = new ProgressDialog(this);
    mProgressDlg.setTitle(title);
    mProgressDlg.setMessage(msg);
    mProgressDlg.setProgressStyle(ProgressDialog.STYLE_SPINNER);
    mProgressDlg.show();
  }
  private void setButtonsEnabled(boolean isConnected) {
    mButtonConn.setEnabled(!isConnected);
    mButtonTurnOn.setEnabled(isConnected);
    mButtonTurnOff.setEnabled(isConnected);
    mButtonPress.setEnabled(isConnected);
    mButtonDisconn.setEnabled(isConnected);
  }

  private void setButtonsVisibility(boolean visible) {
    int v = (visible)? View.VISIBLE : View.INVISIBLE;
    mButtonConn.setVisibility(v);
    mButtonTurnOn.setVisibility(v);
    mButtonTurnOff.setVisibility(v);
    mButtonPress.setVisibility(v);
    mButtonDisconn.setVisibility(v);
  }

  private void setTvColor(int color) {
    mTvRssi.setTextColor(color);
    mTvAddr.setTextColor(color);
  }
}

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="jp.klab.SwitchBot01">
  <uses-permission android:name="android.permission.BLUETOOTH"></uses-permission>
  <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"></uses-permission>
  <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"></uses-permission>
  <uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
  <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity
      android:name=".MainActivity"
      android:label="@string/app_name"
      android:configChanges="orientation|screenSize"
      android:screenOrientation="portrait"
      android:theme="@style/AppTheme.NoActionBar">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
    <!-- ATTENTION: This was auto-generated to add Google Play services to your project for
       App Indexing.  See https://g.co/AppIndexing/AndroidStudio for more information. -->
    <meta-data
      android:name="com.google.android.gms.version"
      android:value="@integer/google_play_services_version" />
  </application>
</manifest>

ESP32 ボードで操作してみる

Android アプリからの操作を確認したところで、次にマイコン方面からのコントロールを試すことにしました。 手元にある BLE モジュールの中で BLE セントラル機能に対応しているのは ESP32 ボードの ESP32-DevKitC だけなので選択の余地なくこれを使います。

ESP32 用の BLE セントラルコードは以前 ESP-IDF ベースで何度か作成したことがありますが、生産性は Arduino IDE + Arduino core for the ESP32 環境のほうが高いためできれば後者を利用したいと考えました。

幸い、ESP 界隈で著名な Neil Kolban さんが ESP32 Adruino core 用の高機能な BLE ライブラリ「ESP32 BLE for Arduino」を 2017年9月より公開していることを知り、さっそく examples ディレクトリ下の BLE_client.ino のコードを下敷きにプログラムを実装してみました。 プログラム起動時に BLE ペリフェラルからのアドバタイズパケットをスキャンし、自分の SwitchBot を見つけたら接続を確立して「Press」コマンドを一度発行する簡潔な内容としました。
動作の様子とソースコードです。動画ではボード上のリセットボタンを三度押下しています。

動画:22秒 音量注意

BLE_SwitchBot01_BLE.ino

/**
 * 
 * BLE_SwitchBot01_BLE.ino  (for ESP32)
 * 
 * BLE セントラル. SwitchBot からのアドバタイジングを検出したら
 * 一度だけ同 GATT サーバへ接続〜所定のキャラクタリスティックへ
 * Press コマンドを書き込んでアームを動かす.
 * 
 * Arduino core for ESP32 向けの Neil Kolban 氏による
 * 下記 BLE ライブラリを使用
 * https://github.com/nkolban/ESP32_BLE_Arduino
 *
 */

#include "BLEDevice.h"
#include <HardwareSerial.h>

// 手元の SwitchBot のアドレス
static String addrSwitchBot = "c0:65:9a:7d:61:e1";
// SwitchBot のユーザ定義サービス
static BLEUUID serviceUUID("cba20d00-224d-11e6-9fb8-0002a5d5c51b");
// 上記サービス内の対象キャラクタリスティック
static BLEUUID    charUUID("cba20002-224d-11e6-9fb8-0002a5d5c51b");
// SwitchBot の Press コマンド
static uint8_t cmdPress[3] = {0x57, 0x01, 0x00};

static BLEAddress *pGattServerAddress;
static BLERemoteCharacteristic* pRemoteCharacteristic;
static BLEClient*  pClient = NULL;
static boolean doSendCommand = false;

// BLEDevice::init() でのシリアルポート混乱対策
static HardwareSerial hs2(2);

void dbg(const char *format, ...) {
  char b[512];
  va_list va;
  va_start(va, format);
  vsnprintf(b, sizeof(b), format, va);
  va_end(va);
  hs2.print(b);
}

// アドバタイズ検出時のコールバック
class advdCallback: public BLEAdvertisedDeviceCallbacks {
  void onResult(BLEAdvertisedDevice advertisedDevice) {
    dbg("BLE device found: ");
    String addr = advertisedDevice.getAddress().toString().c_str();
    dbg("addr=[%s]\r\n", addr.c_str());
    // SwitchBot を発見
    if (addr.equalsIgnoreCase(addrSwitchBot)) {
      dbg("found SwitchBot\r\n");
      advertisedDevice.getScan()->stop();
      pGattServerAddress = new BLEAddress(advertisedDevice.getAddress());
      doSendCommand = true;
    }
  }
};

void setup() {
  hs2.begin(115200, SERIAL_8N1, 16, 17);
  Serial.begin(115200);
  dbg("start");
  // BLE 初期化
  BLEDevice::init("");
  // デバイスからのアドバタイズをスキャン
  BLEScan* pBLEScan = BLEDevice::getScan();
  pBLEScan->setAdvertisedDeviceCallbacks(new advdCallback());
  pBLEScan->setActiveScan(true);
  pBLEScan->start(30); 
}

void loop() {
  if (doSendCommand == true) {
    if (connectAndSendCommand(*pGattServerAddress)) {
    } else {
      dbg("connectAndSendCommand failed");
    }
    doSendCommand = false;
    dbg("done");
  }
  delay(1000);
}

// SwitchBot の GATT サーバへ接続 〜 Press コマンド送信
static bool connectAndSendCommand(BLEAddress pAddress) {
  dbg("start connectAndSendCommand");
  pClient  = BLEDevice::createClient();

  pClient->connect(pAddress);
  dbg("connected\r\n");

  // 対象サービスを得る
  BLERemoteService* pRemoteService = pClient->getService(serviceUUID);
  if (pRemoteService == nullptr) {
    dbg("target service not found\r\n");
    return false;
  }
  dbg("found target service\r\n");

  // 対象キャラクタリスティックを得る
  pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID);
  if (pRemoteCharacteristic == nullptr) {
    dbg("target characteristic not found");
    return false;
  }
  dbg("found target characteristic\r\n");

  // キャラクタリスティックに Press コマンドを書き込む
  pRemoteCharacteristic->writeValue(cmdPress, sizeof(cmdPress), false);

  delay(3000);
  if (pClient) {
    pClient->disconnect();
    pClient = NULL;
  }
  return true;
}

SwitchBot をインターネットごしに操作する

ESP32 といえば・・

上記のように ESP32 ボードで適切に SwitchBot を操作可能であることが確認できました。そこでふと、ここにインターネットアクセスの要素を絡めることを思いつきました。 ESP32 が備えている BLE 機能と WiFi 機能をパラレルに利用できることは以前の試みを通じて確認ずみです。そのため「インターネットごしに所定のメッセージを受信したら SwitchBot を作動させる」ことができるのではないかと考えました。もちろん SwitchBot 公式のインターネットハブ製品である「SwitchBot Hub」のように高機能なものには到底及ばないにしても、ESP32 ボードを使えば最低限の遠隔操作は実現できそうです。

道具立て

インターネット経由での ESP32 へのメッセージングには MQTT を利用することにしました。

CloudMQTT

MQTT ブローカーは最近 Beebotte が流行っているようですが、いくつかのサービスを試し今回はもっとも手に馴染んだ「CloudMQTT」を選びました。無料枠も手元の要件には十分です。

HTTP to MQTT bridge

ノーマルな HTTP リクエストの体裁でメッセージを Publish できれば何かと便利です。Beebotte とは違い現時点で CloudMQTT に用意されている REST API は管理用のもののみですが、petkov さんによる「HTTP to MQTT bridge」の存在とこれを Heroku から利用できることを知りました。ちなみに、Heroku には CloudMQTT 用のアドオンも用意されていますが、ざっと説明を読んだところでは単に CloudMQTT 上のインスタンスを呼び出す内容のようで HTTP to MQTT bridge の利用と比べ際立ったメリットが見えないことと、また、Heroku でアドオンを利用するにはクレジットカード登録が必要である点が若干微妙でもあり今回は利用を見合わせました。

IFTTT

HTTP クライアントから ESP32 へメッセージを送る分には以上の準備で事足ります。一方、SwitchBot のメインのキーワードである「スマートホーム」にはこのところ「スマートスピーカー」がぴたりと寄り添っている感がありますね。なので、お約束どおり(?)手元の Google Home Mini 用に IFTTT アプレットを用意しました。トリガーには Google Assistant サービスを、アクションには Webhooks サービス(旧 Maker チャネル)を割り当て、後者へ上のブリッジへのリクエストを設定します。

コードサイズの超過と対処

引き続き Nick O'Leary さんによる Arduino Client for MQTT ライブラリを利用して CloudMQTT に対向しメッセージの Publish / Subscribe を行うシンプルなスケッチを作成、ESP32 上で問題なく動作することを確認しました。 これと前掲の BLE セントラルプログラムのふたつを軸として結合し細かい処理を書き加えれば完成でしょう。

まずはラフに結合してみました。すると、コンパイルは通ったものの ESP-WROOM-32 のフラッシュメモリへの書き込みでエラーが発生しました。コードサイズの超過です。

最大1310720バイトのフラッシュメモリのうち、スケッチが1568826バイト(119%)を使っています。

最大294912バイトのRAMのうち、グローバル変数が70452バイト(23%)を使っていて、ローカル変数で224460バイト使うことができます。
スケッチが大きすぎます。http://www.arduino.cc/en/Guide/Troubleshooting#size には、小さくするコツが書いてあります。
ボードESP32 Dev Moduleに対するコンパイル時にエラーが発生しました。

あらためて元のふたつのスケッチのコードサイズを確認してみると、内容的にはシンプルな BLE プログラム側が単体で書き込み可能容量の 94%にも及んでいました。ESP32 BLE for Arduino ライブラリのボリュームの大きさが窺えます。

最大1310720バイトのフラッシュメモリのうち、スケッチが1237529バイト(94%)を使っています。

以下を試しました。

  • Arduino 公式のガイダンスとネット上の情報を参考にコーディングレベルでの対処を実施
    • 結果:数10KB程度の削減に留まった
  • ESP32 BLE for Arduino のアーカイブページから、もっともスリムな初版の ESP32_BLE_Arduino-0.1.0.zip(2017-09-10) へのバージョンダウンを試す
    • 結果:すでに現行の Arduino core for the ESP32 とは互換性がなくコンパイルエラーが多発。手元で随所に手を加え最終的にビルド可能となったが当該スケッチのコードサイズは書き込み可能容量対比 115% 程度までにしか収まらず
  • ネット上の情報にそってフラッシュメモリ上の書き込み可能領域を増やしてみる
    • 結果:パーティションテーブルの編集によりコードの書き込みには成功した。だがプログラムは正常に動作せずリブートを繰り返した

現時点では他にこれといった対策も見当たらず Arduino IDE 環境での開発はここで一旦断念しました。

ESP-IDF 環境への移行 〜 完成

こういった経緯を経てネイティブの ESP-IDF 環境へ移行しプログラムを書き直しました。 Espressif はコアライブラリを頻繁に更新しており API の後方互換性が必ずしも維持されないケースのあることが気がかりではありますが、その点は現在もなお活火山状態にある ESP32 向けの開発の宿命と割り切るべきでしょう。

準備

まず ESP-IDF をこの時点での最新版へアップデートしました。

~/esp/esp-idf$ git describe
v3.0-dev-2561-g358c822

MQTT ライブラリは Tuan さんによる「ESP32 MQTT Library」を使用することに。README の記述にそって <ESPIDF>/components/espmqtt へ Git サブモジュールとして一式を追加しました。

処理の流れ

プログラムは以下の内容としました。

  1. WiFi 接続を確立
  2. MQTT セッションの開始
  3. トピック "msg" の メッセージ "1" を受信したら以下の BLE セントラル処理を発動
    • アドバタイズパケットのスキャンを実行
    • 手持ちの SwitchBot からのアドバタイズを検知したらスキャンを停止して接続を確立
    • SwitchBot のサービス一覧の取得を開始
    • サービス cba20d00-224d-11e6-9fb8-0002a5d5c51b を発見したらその配下のハンドルの範囲を記憶
    • サービス一覧の取得が完了したら上記サービス配下のキャラクタリスティック cba20002-224d-11e6-9fb8-0002a5d5c51b のハンドルを取得
    • 上記キャラクタリスティックへ Press コマンド {0x57, 0x01, 0x00} を書き込んで切断
  4. 上記 3. へ

動作の様子

動画:34秒 音量注意 (※ ウェイクワード「ねえ, グーグル」の発声もあります)

ソースコード


(tanabe)
klab_gijutsu2 at 04:44|この記事のURLComments(0)
2017年08月31日

micro:bit と ESP32 でインターネットボタンを作る

はてなブックマークに登録

今月(2017年8月)5日に国内で正式販売の始まった BBC micro:bit を楽しんでいます。遅ればせながら、世界中の子どもと大人に人気のこの素敵なコンピュータのファンになりました。


http://www.bbc.com/
http://www.kitronik.co.uk/

私が micro:bit に関心を持ったのは最近扱っていた話題にちょうどぴったりのデバイスだったことがきっかけでした。今回はその内容と手元での試作を順をおって紹介します。

BLE ボタンデバイスをインターネットボタンとして使うこと

Amazon Dash Button の成功がひとつの契機にもなり この数年の間にインターネットボタンの利用と普及が進みました。手軽さといえば何かとスマホが引き合いに出されることの多い状況あってより単純な操作でより簡単に所定の処理を呼び出せることが専用の機器であることの最大の強みでしょう。

ただその一方で、機器の性質上ボタンデバイスの多くが電池駆動式であることが今後の用途・要件の拡大に伴いより悩ましい要素となる可能性も考えられます。限られた貴重な電力を使って自力でインターネット上の所定のサービスを叩きに行くのは必ずしも効率の良いやり方でありませんから、この点への対処が今後の課題となるかもしれません。

初代の Amazon Dash Button に高容量ではあるものの傍目にもコストバランスの危なげな米エナジャイザー社製の高価なリチウム一次電池が内蔵されていたことは象徴的です

一案として、省電性に優れた BLE ボタンデバイスと仲介役のエージェントデバイスを組み合わせてインターネットボタンを構成する形を想定しました。図にしてみます。

このように考え方はとてもシンプルです。ボタンデバイスは電池駆動、据え置きのエージェントデバイスは AC 電源からの給電を想定しています。 ボタン側は BLE 通信で必要最小限の通知をエージェントへ送り、エージェント側はそれをトリガーに所定のインターネットサービスを呼び出します。インターネットボタンは通常屋内空間での利用が前提ですから、BLE と WiFi の通信距離の違いに留意して機器の設置・運用を行えばこの構成には相応の合理性があります。また、小さなボタンデバイスひとつに複雑な処理を入れ込むことに比べ、機器の組み合わせによって必要な機能を実現するやり方には柔軟性が見込まれるでしょう。

通知の方法と道具立て

通知方法について

通常、BLE ペリフェラルからセントラルへの通知はデバイス間の接続確立後に所定の手続きによりセットアップを行うことで実施可能となります。この設定は両者の接続が維持されている間のみ有効です。

もちろんそれは BLE の仕様上のまっとうな規約なのですが、前掲の案への適用にはあまり気が進まずにいました。理由は以下の三点です。

  1. 「接続の維持」が必須であることは BLE の省電性を最大限に活かそうとする主旨と必ずしも折り合わない
  2. エージェントへの通知は必要最小限の合図のみでよいにもかかわらず仕掛けが豪華すぎる
  3. 技術情報や SDK の公開されていないボタンデバイスを利用したい場合にはグレーな解析が必要

代わりに デバイスからのアドバタイジングパケットを合図として利用することを考えました。この方法ならよりコストが低くデバイス間の接続に拘束されることもありません。また、アドバタイズの内容は文字通りオープンなので一般的なツールで普通に読めるという間口の広さも利点です。
注意の必要な点としては、連続して発生するアドバタイジングパケットにエージェント側が過剰に反応しないようにすることと、当該デバイスを本件専用として扱うべきであることが挙げられるでしょう。

エージェントについて

前掲の案のポイントは BLE 通信とインターネット通信の両方をカバーするエージェントを用意する点にあります。BLE つきの遊んでいるスマホを利用する手もありますが、「据え置き」「常時稼働」を前提とする IoT 装置として利用するにはいささかオーバースペックの感があり、また、視点は異なるもののスマホ系とボタンの連携に関しては「Flic」や「Pochiru」のような先例もあるためもうひとつ食指が動きません。

そこで ESP32 モジュールを使うことにしました。ESP-WROOM-32 は今回の要件を単体ですべて満たしているためまさに適役で価格面での魅力もあります。

ボタンについて

過去にこのブログでピックアップしたもの以外にも手元には複数の BLE ボタンデバイスがあります。それぞれに個性があり並べて使ってみるとなかなか面白いのですが、上述のようにアドバタイジングパケットを利用することを想定すると次のような機能・機構がほしいと考えました。

  1. 給電中にアドバタイズの開始・停止を任意に制御できること
  2. 複数のボタンが装備されていること
    (「ひとつのデバイスでひとつの処理」ではちょっと寂しい。アドバタイズを合図とする以上、ボタンデバイスが本来の通知機能においてダブルプッシュや長押しといったボタンアクションの区別に対応していてもそれを利用することができないという事情もある)
  3. 上記 2. に関連して、アドバタイズデータの内容を変更可能であること

残念ながらこれらの要件を満たすものは手元にありませんでしたが、プロトタイピングであれこれ試したところではどのボタンもそれなりに使えるのでまあこれはこれで・・とも思っていました。そこでたまたま目にしたのが micro:bit 国内販売のニュースです。

それまでこの製品のことは名前しか知らずにいました。あらためて写真をみてふたつのタクトスイッチが目にとまり、さらに記事中の「技適」の文字が気になって情報を調べたところ実は上のみっつの要件をすべて満たしていることがわかったため迷わずこれを利用することにしました。
ちなみにスイッチサイエンスさんが去年 12月にリリースした chibi:bit のこともこの時に知りました。ショップにはよくお世話になっているのですが、一見あまり関係のなさそうなところにしっかり良いものがあったりするものですね。

micro:bit 用に追加購入したパーツ

micro:bit ボードをボタンデバイスとして扱うために欠かせないふたつのパーツを調達しました。 いずれも英 Kitronik 社の製品で、ボタン電池から給電するためのオプションボード「MI:power(直販価格 £4.16)と、同ボードを装着した状態の全体をすっぽり覆うことのできる「MI:pro Protective Case(直販価格 £4.10)です。写真はケースに収めた状態の外観です。


前面
背面
また、今回は使っていませんが、拡張コネクタ用の「Edge Connector Breakout Board(直販価格 £4.15)もあわせて入手しました。

余談ながら Kitronik 社は直販を行ってはいるものの日本への送料が最安でも £30.95 と高いため、送料の安いショップを探して英国内の Pimoroni にたどり着き、そこで「MI:power」と「Edge Connector Breakout Board」を購入しました。送料は £5.50。8月7日に発注し 1週間で到着しました。なお、このふたつの製品はスイッチサイエンスさんのサイトにも掲載されており 8月21日に同社から「ご注文いただけるようになりました」メールが届きました。

どうしても欲しかった「MI:pro Protective Case」は Pimoroni にも見当たらず、しつこい検索を経て結局その時点で送料を含め最も安く販売していた ebay 出品企業から買いました。それでも総額が日本国内での micro:bit 本体価格よりも高額ではありましたが、ちょうど 2週間後の 8月25日に届き嬉々として使っています。

スイッチサイエンスさん、ぜひこの「MI:pro Protective Case」も扱って下さい!Pimoroni さんも今のところ扱っていないようですが、もし入荷されたら少なくとも私は確実に買います!(^^;

(追記)2017年10月11日にスイッチサイエンスさんサイトでの販売が開始されました。
  MI:電源ボード用 MI:proケース - コード番号 KITRONIK-5611

実装

micro:bit 側

micro:bit 側のプログラムは Microsoft MakeCode で手早く作成しました。以下の内容としています。

  • 電源が入ると最上段中央の LED を 2秒間隔で点滅開始 (給電状態の確認用)
  • 左側のボタン A が押されるとボタン右の LED を点灯し Eddystone-URL パケットにダミー URL "http://A" をのせて 4秒間アドバタイズする
  • 右側のボタン B が押されるとボタン左の LED を点灯し Eddystone-URL パケットにダミー URL "http://B" をのせて 4秒間アドバタイズする

アドバタイズに Eddystone-URL パケットを使用しているのは MakeCode 上の「Bluetooth」の項に他の選択肢が見当たらなかったためです。これと言って弊害も想定されず、また、どのような形式であれデータの内容で A, B ボタンを区別できれば事足りるのでこれはこれでよしとしました。図はエディタ画面のスクリーンショットです。すでにこういう時代なのですね。 (クリックで可読大表示)

ESP-WROOM-32 側

ESP32 モジュール側のプログラムは ESP-IDF 環境で作成しました。

処理の概要

  1. 所定の WiFi アクセスポイントへ接続
  2. 所定の URL へ HTTPS アクセスを行うためのタスクを生成して待機
    〜 下記 3. での要求発生時にリクエスト送出とレスポンス受信を行い終われば再び待機状態へ
  3. 永続ループで BLE アドバタイジングパケットのスキャンを開始
    〜 対象とする MAC アドレスのデバイスからの対象とする符丁を含む Eddystone-URL パケットを検知すると上記 2. のタスクへ所定の URL へのリクエストを要求

ソースコード

ここでは一台の micro:bit のみを対象としていますが、ソースコード中の Entries テーブルへ適宜エントリを追加することで複数の micro:bit を対象とすることが可能です。

A, B ボタンの押下に呼応するアクションのサンプルとして Gmail 発信, 携帯電話のコールのふたつの処理を紐付けています。これらはいずれも Microsoft Flow 上に作成したフローの URL への GET 要求によって実現しています。ふたつのフローのスクリーンショットを以下に示します。 (クリックで可読大表示)

Gmail 発信

電話機をコール(Twilio API を利用)

動作の様子

動画:1分18秒 (途中で電話着信音が鳴ります。音量に注意して下さい)


(tanabe)
klab_gijutsu2 at 09:06|この記事のURLComments(2)
2016年11月07日

MaBeee をダミー電池で利用する

はてなブックマークに登録

MaBeee のこと

MaBeee が気に入っています。
MaBeee は単四電池を装着することで無線制御可能な単三電池として振る舞うユニークな BLE(Bluetooth Low Energy)デバイスです。昨年(2015年)末に Makuake に登場して注目を集め今年8月に一般販売が始まりました。給電のオンオフや出力電圧レベルをスマホやタブレットから簡単にコントロールできるためホビー系の機器などで活用している方も多いことでしょう。。


http://mabeee.mobi/

手軽で実用性のある MaBeee ですが、パワーの弱い単四電池での稼働が前提であることを残念に思うことがありました。もちろん、そもそも MaBeee はこのようなスタイルを実現するためにこそ緻密にデザインされた製品なのであくまでも一利用者のわがままではありますが、一方で、より容量の大きいバッテリーや AC 電源で腰を据えて使用できればこの製品の用途がさらに拡がると考えました。そこで役に立ったのがダミー電池です。

ダミー電池?

ダミー電池は乾電池と同じサイズ・形状をしており、内部でプラス極とマイナス極が直結しているタイプが基本です。当然ながら充電器への装着は厳禁。電圧調整等の目的に利用され、その方面ではメジャーなアイテムであることを知りました。自作する向きもあるようです。

http://store.shopping.yahoo.co.jp/ http://eleshop.jp/ http://www.amazon.co.jp/

工作向けのダミー電池と MaBeee

上のように両極をショートさせ全体をシールドしているもの以外に、自分で配線したり回路を組み込んだりすることのできるもタイプもあります。これを MaBeee への給電用に使えば良さそうです。ただ、このタイプは種類が少なく一般のメーカーやパーツ屋さんの扱っているものの中に単四型はまず見かけません。


http://www.marutsu.co.jp/

単四サイズのものを自作する選択もありますが、ただでさえ精密でデリケートな MaBeee を何かの拍子に傷めるリスクを考えるとあまり気が進まず、ネットを探して以下の製品に行き着ました。工作機械で加工を施したプラスティック製品や機械本体を製造・販売している「工房モコ」さんによる貴重なダミー電池のラインナップです。

「工房モコ」こんな物いかが? - www.geocities.jp/moco7jp


●ハイパーダミー電池(高機能にしていただくタイプ)

ハイパーダミー電池はユーザーが内部に回路を仕込んで高機能にしていただくタイプです。
抵抗を入れたり、ダイオードを入れたり、回路をいれたり、機器を外部電源で使いたい時、外部電源との橋渡しに、・・・ どんな工夫ができるかな。
発生した損害は自己責任でお願いします。

電池の種類は、次の6種類を用意しています。
 単一 単二 単三 単四 単五 CR123A

内部のH形のブリッジ部は、不要ならニッパーでカットすれば取り除けます。
4ケ所のくびれた所をカットして下さい。御希望によりカットして出荷します。

さっそく単四型のものを購入しました。シンプルな作りながら扱いやすいように工夫されており仕上げも丁寧です。

単四型の現物 給電用のコードを取り付ける MaBeee へ装着し電池ボックスへ

ともに「バーチャルな乾電池」である MaBeee とダミー電池の取り合わせ。何やら連想が膨らみます。


(tanabe)
klab_gijutsu2 at 10:44|この記事のURLComments(0)TrackBack(0)
2016年08月30日

既成の BLE デバイスを自作プログラムから利用する試み

はてなブックマークに登録

前回の記事:
  • BLE デバイス間の通信内容をパケットレベルで読み解いてみる
    題材には低価格で出回っているありふれた Anti-Lost 系 BLE デバイス A を選びました。この小さなデバイスには LED・ブザーと押しボタンスイッチが 実装されており、対向スマホアプリとの間で双方向のやりとりが可能なつくりになっています。


    デバイス A の UI
前回は BLE パケットスニファを使って既成の BLE デバイス「A」と対向アプリ間の通信内容を記録しその精査を試みました。そこで得られた情報をもとに、今回は実験の一環としてデバイス A と連携する Android アプリの試作を行います。パケットログから窺える BLE セントラル側の所作を自作プログラムで再現することは論理的に可能でしょう。

デバイス A とアプリの連携に必要な処理

前回の記事から、デバイス A とアプリの連携に関する要素をピックアップしてみます。

  • アプリからデバイス A の LED・ブザーをコントロール

    • 「Scene 6: アプリからデバイス A の LED・ブザーを操作」
      Frame 616

      対向アプリ上の所定のボタンを押下すると、Frame 345, Frame 238 の示す、 "「Immediate Alert」サービス配下の「Alert Level」キャラクタリスティック" の Value Handle 0x0025 に値「2(High Alert)」が書き込まれる

      デバイス A の LED・ブザーはファームウェアレベルでこの Alert Level キャラクタリスティックに紐づけられている模様。High Alert 値が書き込まれるとそれに反応して短時間 LED とブザーが ON になる

    ー> デバイス A の提供する Immediate Alert サービス配下の Alert Level キャラクタリスティックにアプリから値 2 を書き込めばデバイス A の LED・ブザーが ON になり、値 0 を書き込めば OFF になる

  • デバイス A のボタン押下をアプリへ通知

    • 「Scene 5: デバイス A ボタン押下時のアプリへの通知を設定」
      Frame 491

      Frame 453、Frame 439、Frame 453 の示す通り、ハンドル 0x0036 は、 "ユーザ定義サービス 1(UUID = 0xFFE0)配下のユーザ定義キャラクタリスティック(UUID = 0xFFE1)配下の Client Characteristic Configuration Descriptor (CCCD)" である

      当該ユーザ定義キャラクタリスティック(UUID = 0xFFE1)のプロパティには Notify が設定されており、クライアントである対向アプリから CCCD 0x0036 に Notification bit (0x0001) を書き込んでおくことで、このキャラクタリスティックの値が更新された時に Value Handle である 0x0035 経由でアプリ側へ通知(Notification)が行われるようになる

    • 「Scene 7: デバイス A のボタンを押すとアプリへ通知」
      Frame 711

      デバイス A の物理ボタンはファームウェアレベルで Frame 439 の示す "ユーザ定義サービス 1(UUID = 0xFFE0)配下のユーザ定義キャラクタリスティック(UUID = 0xFFE1)" に紐づけられている模様。このボタンを押すと当該キャラクタリスティックの値がデバイス内部で更新され Frame 491 での仕込みに基づきアプリ側へ通知が行われる

    ー> 初期処理として、デバイス A の提供するユーザ定義サービス 1(UUID = 0xFFE0)配下のユーザ定義キャラクタリスティック(UUID = 0xFFE1)の持つ Client Characteristic Configuration Descriptor (詳細:1, 2) にアプリから値 0x0001を書き込んでおく。それ以降にデバイス A のボタンが押下されると当該キャラクタリスティック経由でデバイス A からアプリへ通知が行われる
処理そのものには特に難しそうな要素もなく、アプリからデバイス A への接続後に所定のキャラクタリスティック・デスクリプタの操作を適切に行うことがポイントとなりそうです。

サービス・キャラクタリスティック・デスクリプタの UUID について

BLE ネイティブの世界では所定のキャラクタリスティックやデスクリプタの I/O には各エントリに紐づけられたハンドルが使用されますが、抽象化された Android API での処理対象はオブジェクトです。所定のキャラクタリスティックやデスクリプタのオブジェクトの取得にはそれぞれの UUID が必要であるため、プログラムの記述に際しては、パケットログ上に記録された所定のハンドルがどの UUID のエントリのものであるかを正確に把握する必要があります。
また、サービス - キャラクタリスティック - デスクリプタは階層関係にあるため、所定のエントリのオブジェクトを取得する手続きは最上位にあるサービスのオブジェクトが常に起点となります。

以下に、パケットログから所定のサービス以下の各エントリの UUID を見つける方法と、各 UUID からそれぞれのエントリのオブジェクトを取得する Android コードの例を示します。

パケットログから所定のサービス以下の各エントリの UUID を拾う

  • 前回記事中のパケットログ Frame 229 では 0x0001 - 0xffff のハンドル空間を対象に Read By Group Type Request で GATT Primary Service Declaration を照会、そのレスポンスが Frame 231 です

           (クリックで大きく表示)

    ここではレスポンスに含まれる 3件のレコードのうち 2件めに注目してみます

    Opcode: Read By Group Type Response (0x11)
             :
    
    Attribute Data, Handle: 0x000c, Group End Handle: 0x000f
      Handle: 0x000c
      Group End Handle: 0x000f
      Value: 0118
             :
    
    • Primary Service Declaration の照会に対する Read By Group Type Response の「Value」には当該サービスの UUID が格納される
    • この例では 16ビット UUID = 0x1801 であり、これは、BLE 既定の Generic Attribute サービスを示す
    • 当該サービスはハンドルグループ 0x000c - 0x000f を占有する
  • Frame 287 では上記のハンドルグループ 0x000c - 0x000f を対象に Read By Type Request で GATT Characteristic Declaration を照会、そのレスポンスが Frame 289 です

           (クリックで大きく表示)

    レスポンスに注目します

    Opcode: Read By Type Response (0x09)
             :
    
    Attribute Data, Handle: 0x000d
      Handle: 0x000d
      Value: 200e00052a
    
    • Characteristic Declaration の照会に対する Read By Type Response の「Value」には当該キャラクタリスティックのプロパティ・Value Handle・UUID が格納される(詳細:1a, 2a, 1b

      (※表は BLUETOOTH SPECIFICATION Version 4.2 [Vol 3, Part G] page 532 より)
    • ここでは「value: 200e00052a」につき、プロパティ = 0x20 (Indicate), Characteristic Value Handle = 0x000e, 16ビット UUID = 0x2A52
    • 16ビット UUID = 0x2A52 は、BLE 既定の Service Changed キャラクタリスティックを示す
  • Frame 302 では残りのハンドル 0x000f についての情報を GATT へ Find Information Request で照会、そのレスポンスが Frame 305 です

           (クリックで大きく表示)

    レスポンスに注目します

    Opcode: Find Information Response (0x05)
    UUID Format: 16-bit UUIDs (0x01)
    Handle: 0x000f
    UUID: Client Characteristic Configuration (0x2902)
    
    • ハンドル 0x000f は Client Characteristic Configuration Descriptor (CCCD) であり、この CCCD は直前の Indicate プロパティを持つ Service Changed キャラクタリスティックに属する
      (※CCCD の 16ビット UUID は 0x2902 固定

以上のことから、デバイス A 上の Generic Attribute サービスの構成は以下の内容であることがわかります。

Generic Attribute サービス(UUID = 0x1801)
  |  Handle Group = 0x000c - 0x000f
 |
  +-- Service Changed キャラクタリスティック(UUID = 0x2A52)
        |   Handle = 0x000d, Value Handle = 0x000e
        |
        +-- Client Characteristic Configuration Descriptor (UUID = 0x2902)
              Handle = 0x000f

所定の UUID のエントリのオブジェクトを取得する

ここまでに登場した UUID はすべて 16ビット値でしたが、16 ビット UUID は Bluetooth 用にアサインされている本来の 128ビット UUID の固定部分(BASE_UUID)を省略した表現です。

  • BLUETOOTH SPECIFICATION Version 4.2 [Vol 3, Part B] page 227
    2.5 SEARCHING FOR SERVICES
           :
    2.5.1 UUID
           :
    To reduce the burden of storing and transferring 128-bit UUID values, 
    a range of UUID values has been pre-allocated for assignment to 
    often-used, registered purposes. The first UUID in this pre-allocated
    range is known as the Bluetooth Base UUID and has the value 
    00000000-0000-1000-8000-00805F9B34FB,
           :
    
つまり、Generic Attribute サービスの 16ビット UUID「0x1801」は、128ビット UUID「00001801-0000-1000-8000-00805F9B34FB」です。

Android API を用いて前出の Generic Attribute サービスと Service Changed キャラクタリスティック、およびその配下の Client Characteristic Configuration Descriptor のオブジェクトを取得するコードのイメージを示します。

private BluetoothGatt mBtGatt;

private BluetoothGattCharacteristic mChServiceChanged;
private BluetoothGattDescriptor mCCCD;

// Generic Attribute サービス の UUID
private UUID mUuidSvcGenericAttribute = UUID.fromString("00001801-0000-1000-8000-00805f9b34fb");
// Service Changed キャラクタリスティックの UUID 
private UUID mUuidChServiceChanged    = UUID.fromString("00002a52-0000-1000-8000-00805f9b34fb");
// Client Characteristic Configuration Descriptor の UUID (固定値)
private UUID mUuidCCCD                = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
                 :

private BluetoothGattCallback mGattCallback = new bleGattCallback();
                 :

// デバイス A へ接続後、GATT の提供する各サービス以下の一覧を取得
mBtGatt.discoverServices();(mCtx, false, mGattCallback);
                 :

// GATT イベントハンドラ
private class bleGattCallback extends BluetoothGattCallback {
  @Override
  // GATT サービス一覧取得完了
  public void onServicesDiscovered(BluetoothGatt gatt, int status) {
    super.onServicesDiscovered(gatt, status);

    // デバイス A の Generic Attribute サービスの
    // Service Changed キャラクタリスティックオブジェクトを取得
    BluetoothGattService svc = gatt.getService(mUuidSvcGenericAttribute);
    mChServiceChanged = svc.getCharacteristic(mUuidChServiceChanged);

    // Service Changed キャラクタリスティックの
    // Client Characteristic Configulation Descriptor を取得
    mCCCD = mChServiceChanged.getDescriptor(mUuidCCCD);
                 :
  }

ちなみに、アプリ開発の初期に BluetoothGattCallback の onServicesDiscovered() に次の要領のコードを挿入して 各 GATT サービス配下の全エントリの UUID を階層的に出力し保存しておくと何かと便利です。

public void onServicesDiscovered(BluetoothGatt gatt, int status) {
  super.onServicesDiscovered(gatt, status);

  List<BluetoothGattService> serviceList = gatt.getServices();

  Log.d(TAG, "onServicesDiscovered: serviceList.size=" + serviceList.size());

  for (BluetoothGattService s : serviceList) {
    Log.d(TAG, "onServicesDiscovered: svc uuid=" + s.getUuid().toString());
    List<BluetoothGattCharacteristic> chlist = s.getCharacteristics();
    Log.d(TAG, "onServicesDiscovered: chrlist.size=" + chlist.size());

    for (BluetoothGattCharacteristic c : chlist) {
      UUID uuid = c.getUuid();
      Log.d(TAG, "onServicesDiscovered:  chr uuid=" + uuid.toString());
      List<BluetoothGattDescriptor> dlist = c.getDescriptors();

      Log.d(TAG, "onServicesDiscovered:  desclist.size=" + dlist.size());
      for (BluetoothGattDescriptor d : dlist) {
        Log.d(TAG, "onServicesDiscovered:   desc uuid=" + d.getUuid());
      }
    }
  }

デバイス A への接続後に上のコードを実行した際のログです。(※見やすさのためにサービスごとに改行を挿入)

当然ながら、この内容は前回採取したパケットログ内の各エントリの情報と符合しています。

作成したアプリ

以上の内容にもとづいてアプリを作成しました。ごくシンプルなものですが期待通りに動いています。

ソースコード一式

動画:動作の様子

作成した Android アプリとデバイス A の連携の様子を収めた動画です。デバイス A は UI 部分のみを露出しています。
(34秒 アラーム音あり 音量注意)
    

メモ:実装手順など

private BluetoothAdapter mBtAdapter;
private BluetoothLeScanner mBtScanner;
private BluetoothDevice mBtDevice;
private BluetoothGatt mBtGatt;

1. BluetoothAdapter 〜 BluetoothLeScanner を取得

  • BluetoothAdapter - developer.android.com
    Represents the local device Bluetooth adapter. The BluetoothAdapter lets you
    perform fundamental Bluetooth tasks, such as initiate device discovery, query
    a list of bonded (paired) devices, instantiate a BluetoothDevice using a known
    MAC address, and create a BluetoothServerSocket to listen for connection
    requests from other devices, and start a scan for Bluetooth LE devices.
                      :
    
    static BluetoothAdapter	getDefaultAdapter()
    
    Get a handle to the default local Bluetooth adapter. 
    
  • BluetoothAdapter - getBluetoothLeScanner - developer.android.com
    BluetoothLeScanner getBluetoothLeScanner ()
    
    Returns a BluetoothLeScanner object for Bluetooth LE scan operations. 
    
    • BluetoothLeScanner - developer.android.com
      BluetoothLeScanner
      
      This class provides methods to perform scan related operations for Bluetooth
      LE devices. An application can scan for a particular type of Bluetooth LE
      devices using ScanFilter. It can also request different types of callbacks
      for delivering the result. 
      
mBtAdapter = BluetoothAdapter.getDefaultAdapter();
mBtScanner = mBtAdapter.getBluetoothLeScanner();

2. アドバタイジングパケットのスキャン 〜 対象とする BluetoothDevice を取得

private ScanCallback mScanCallback = new bleScanCallback();
                 :

mBtScanner.startScan(mScanCallback);
                 :

private class bleScanCallback extends ScanCallback {
    @Override
    public void onScanResult(int callbackType, ScanResult result) {
        super.onScanResult(callbackType, result);
        if (.......) {
          mBtDevice = result.getDevice();
        }
    }
    @Override
    public void onScanFailed(int errorCode) {
        super.onScanFailed(errorCode);
        Log.e(TAG, "onScanFailed: err=" + errorCode);
    }
}

3. デバイスへの接続

  • BluetoothDevice - connectGatt - developer.android.com
    BluetoothGatt connectGatt (Context context, 
                    boolean autoConnect, 
                    BluetoothGattCallback callback)
    
    Connect to GATT Server hosted by this device. Caller acts as GATT client. 
    The callback is used to deliver results to Caller, such as connection status
    as well as any further GATT client operations. The method returns a
    BluetoothGatt instance. You can use BluetoothGatt to conduct GATT client
    operations.
                      :
    callback 	BluetoothGattCallback: GATT callback handler that will
    receive asynchronous callbacks.
                      :
    
  • BluetoothGattCallback - developer.android.com
    •  onConnectionStateChange - developer.android.com
      void onConnectionStateChange (BluetoothGatt gatt, 
                      int status, 
                      int newState)
      
      Callback indicating when GATT client has connected/disconnected to/from
      a remote GATT server.
                        :
      newState 	int: Returns the new connection state. Can be one of
      STATE_DISCONNECTED or STATE_CONNECTED
                        :
      
private BluetoothGattCallback mGattCallback = new bleGattCallback();
                 :

mBtGatt = mBtDevice.connectGatt(mCtx, false, mGattCallback);
                 :

private class bleGattCallback extends BluetoothGattCallback {
  @Override
  public void onConnectionStateChange(BluetoothGatt gatt, int status,
                                      int newState) {
    super.onConnectionStateChange(gatt, status, newState);
    if (newState == BluetoothProfile.STATE_CONNECTED) {
      // 接続確立 - デバイスの GATT サービス一覧の取得へ
    } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
      // 切断完了の処理
    }
  }
                 :

4. GATT サービス - キャラクタリスティック - デスクリプタを探索 〜 必要なオブジェクトを取得

  • BluetoothGatt - discoverServices - developer.android.com
    boolean discoverServices ()
    
    Discovers services offered by a remote device as well as their
    characteristics and descriptors.
    
    This is an asynchronous operation. Once service discovery is completed, 
    the onServicesDiscovered(BluetoothGatt, int) callback is triggered. 
    If the discovery was successful, the remote services can be retrieved
    using the getServices() function. 
                      :
    
  • BluetoothGattCallback - onServicesDiscovered - developer.android.com
    void onServicesDiscovered (BluetoothGatt gatt, 
                    int status)
    
    Callback invoked when the list of remote services, characteristics
    and descriptors for the remote device have been updated, ie new services
    have been discovered.
                      :
    
  • BluetoothGatt - getService - developer.android.com
    getService
    
    BluetoothGattService getService (UUID uuid)
    
    Returns a BluetoothGattService, if the requested UUID is supported by
    the remote device.
    
    This function requires that service discovery has been completed for
    the given device.
    
    If multiple instances of the same service (as identified by UUID) exist,
    the first instance of the service is returned.
    
    Requires BLUETOOTH permission.
    
    Parameters
    uuid 	UUID: UUID of the requested service
    Returns
    BluetoothGattService 	BluetoothGattService if supported, or null if the
                            requested service is not offered by the remote device. 
    
    • BluetoothGattService - getCharacteristic - developer.android.com
      getCharacteristic
      
      BluetoothGattCharacteristic getCharacteristic (UUID uuid)
      
      Returns a characteristic with a given UUID out of the list of
      characteristics offered by this service.
      
      This is a convenience function to allow access to a given characteristic
      without enumerating over the list returned by getCharacteristics()
      manually.
      
      If a remote service offers multiple characteristics with the same UUID,
      the first instance of a characteristic with the given UUID is returned.
      
      Parameters
      uuid 	UUID
      Returns
      BluetoothGattCharacteristic  GATT characteristic object or null if no
                                   characteristic with the given UUID was found. 
      
      
      • BluetoothGattCharacteristic - getDescriptor - developer.android.com
        getDescriptor
        
        BluetoothGattDescriptor getDescriptor (UUID uuid)
        
        Returns a descriptor with a given UUID out of the list of descriptors
        for this characteristic.
        
        Parameters
        uuid  UUID
        Returns
        BluetoothGattDescriptor  GATT descriptor object or null if no
                                 descriptor with the given UUID was found. 
        
private BluetoothGattCharacteristic mChAlertLevel = null;
private BluetoothGattCharacteristic mChUser1 = null;
private BluetoothGattDescriptor mDescUser1 = null;

// デバイス A の提供するサービス・キャラクタリスティック群の UUID より
private UUID mUuidSvcImAlert   = UUID.fromString("00001802-0000-1000-8000-00805f9b34fb");
private UUID mUuidChAlertLevel = UUID.fromString("00002a06-0000-1000-8000-00805f9b34fb");
private UUID mUuidSvcUser1     = UUID.fromString("0000ffe0-0000-1000-8000-00805f9b34fb");
private UUID mUuidChUser1      = UUID.fromString("0000ffe1-0000-1000-8000-00805f9b34fb");
// UUID for Client Characteristic Configuration Descriptor
// - BLUETOOTH SPECIFICATION Version 4.2 [Vol 3, Part G] page 537
private UUID mUuidCCCD         = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
                 :

private BluetoothGattCallback mGattCallback = new bleGattCallback();
                 :

mBtGatt.discoverServices();(mCtx, false, mGattCallback);
                 :

private class bleGattCallback extends BluetoothGattCallback {
  @Override
  // GATT サービス一覧取得完了
  public void onServicesDiscovered(BluetoothGatt gatt, int status) {
    super.onServicesDiscovered(gatt, status);

    // デバイス A の Immediate Alert サービスの
    // Alert Level キャラクタリスティックオブジェクトを取得
    BluetoothGattService svc = gatt.getService(mUuidSvcImAlert);
    mChAlertLevel = svc.getCharacteristic(mUuidChAlertLevel);

    // デバイス A のユーザ定義サービス 1 の ユーザ定義キャラクタリスティックの
    // Client ​Characteristic Configulation Descriptor を取得
    svc = gatt.getService(mUuidSvcUser1);
    mChUser1 = svc.getCharacteristic(mUuidChUser1);
    mDescUser1 = mChUser1.getDescriptor(mUuidCCCD);
                 :
  }

5. 所定のキャラクタリスティックからの通知を有効に

// デバイス A への Alert 指示用
private byte[] mCmdAlertOff = new byte[] {(byte)0x00}; // OFF (No Alert)
private byte[] mCmdAlertOn  = new byte[] {(byte)0x02}; // ON (High Alert)
                 :

  // デバイス A のユーザ定義サービス 1 の ユーザ定義キャラクタリスティックの
  // Client ​Characteristic Configulation Descriptor を取得
  svc = gatt.getService(mUuidSvcUser1);
  mChUser1 = svc.getCharacteristic(mUuidChUser1);
  mDescUser1 = mChUser1.getDescriptor(mUuidCCCD);

  // 同キャラクタリスティックの値変更時の通知を有功にして
  // 同 CCCD へ ENABLE_NOTIFICATION_VALUE を書き込んで通知へ待機
  mBtGatt.setCharacteristicNotification(mChUser1, true);
  byte[] val = new byte[] {(byte)0x01, (byte)0x00};
  mDescUser1.setValue(val);
  mBtGatt.writeDescriptor(mDescUser1);
                 :

private class bleGattCallback extends BluetoothGattCallback {
  @Override
  public void onCharacteristicChanged (BluetoothGatt gatt,
                                       BluetoothGattCharacteristic ch) {
    Log.d(TAG, "onCharacteristicChanged");
    // デバイス A のユーザ定義キャラクタリスティック 1 からの通知を受信
    if (ch == mChUser1) {
        Toast.makeText(mCtx, "* P U S H E D *", Toast.LENGTH_SHORT).show();
    }
  }
  @Override
  public void onDescriptorWrite (BluetoothGatt gatt,
                                BluetoothGattDescriptor desc,
                                int status) { // writeDescriptor() 結果
    super.onDescriptorWrite(gatt, desc, status);
    Log.d(TAG, "onDescriptorWrite: sts=" + status);
    if (desc == mDescUser1) {
      // デバイス A のユーザ定義サービス 1 の ユーザ定義キャラクタリスティックの
      // Client ​Characteristic Configulation Descriptor への書き込みが完了
    }
  }
                 :

6. 所定のキャラクタリスティックへの書き込み

// デバイス A への Alert 指示用
private byte[] mCmdAlertOff = new byte[] {(byte)0x00}; // OFF (No Alert)
private byte[] mCmdAlertOn  = new byte[] {(byte)0x02}; // ON (High Alert)
                 :

  mChAlertLevel.setValue(mCmdAlertOn);
  mBtGatt.writeCharacteristic(mChAlertLevel);
                 :

private class bleGattCallback extends BluetoothGattCallback {
  @Override
  public void onCharacteristicWrite(BluetoothGatt gatt,
                                    BluetoothGattCharacteristic ch,
                                    int status) { // writeCharacteristic 結果
    super.onCharacteristicWrite(gatt, ch, status);
    if (ch == mChAlertLevel) { 
      Log.d(TAG, "mChAlertLevel: onCharacteristicWrite: sts=" + status);
    }
  }
                 :

(tanabe)
klab_gijutsu2 at 14:28|この記事のURLComments(0)
2016年08月24日

BLE デバイス間の通信内容をパケットレベルで読み解いてみる

はてなブックマークに登録

前回の記事:
パケットスニファを使って実際に BLE 機器間の応酬を追ってみました。備忘をかねて以下に情報を控えます。題材には低価格で出回っているありふれた Anti-Lost 系 BLE デバイス A を選びました。この小さなデバイスには LED・ブザーと押しボタンスイッチが実装されており、対向スマホアプリとの間で双方向のやりとりが可能なつくりになっています。


デバイス A の UI

操作内容

デバイス A と Android 端末を使って以下の操作を行いスニファで BLE パケットのログを採取しました。

  1. デバイス A の電源を入れる
  2. Android 端末上の対向アプリからデバイスを探索しデバイス A との接続を確立
  3. アプリからデバイス A の LED+ブザーを ON にする
  4. デバイス A のボタンを押しアプリへ通知を送る
  5. アプリ上でデバイス A との接続を切断

応酬の全体像

スニファの Wireshark ログより

上の操作時に収集したパケットログの全体図です。   (クリックで大きく表示)

    

要約

パケットログの内容の要約を以下に示します。   (クリックで大きく表示)

  1. デバイス A の電源を入れる
    • デバイス A がアドバタイジングを開始 (ADV_IND)
  2. スマホ上の対向アプリからデバイスを探索しデバイス A との接続を確立
    • デバイスのスキャン (SCAN_REQ / SCAN_RSP) を経て接続要求 (CONNECT_REQ) 〜接続が確立するとデバイス A はアドバタイジングを停止
    • 機能情報を交換 (LL_FEATURE_REQ / LL_FEATURE_RSP)
    • デバイス A の提供する GATT プライマリサービス一覧を取得
    • 各サービス配下のエントリ(Included Service, キャラクタリスティック)一覧を取得
    • 各キャラクタリスティック配下のデスクリプタを取得
    • デバイス A のボタンが押された時に通知されるよう所定の Client Characteristic Configuration descriptor へアプリから Notification Bit 0x0001 を書き込む
  3. アプリからデバイス A の LED+ブザーを ON にする
    • アプリ上の所定のボタンが押されたタイミングでアプリはデバイス A の Immediate Alert サービス配下の Alert Level キャラクタリスティックへ値 2 (High Alert)を書き込む。これにより一定時間 A の LED とブザーが ON になる
  4. デバイス A のボタンを押しアプリへ通知を送る
    • デバイス A のボタンが押されたタイミングでデバイス A は自機のユーザ定義サービス 1 (UUID=0xFFE0) 配下のユーザ定義キャラクタリスティック (UUID=0xFFE1) へ値 1 を書き込む。当該キャラクタリスティック配下の Client Characteristic Configuration descriptor にはアプリ側から予め Notification Bit がセットされているためアプリへ通知される
  5. アプリ上でデバイス A との接続を切断
    • アプリからデバイス A へ LL_TERMINATE_IND を送る

応酬の詳細

全体の流れを見渡したところで次に個々のパケットデータを読み進めていきます。

  • 以下の記事には Wireshark のスクリーンショットをログとして添えています  (それぞれクリックで大きく表示)
  • ログには BLE パケットデータ本体に加え nRF Sniffer が解析の便宜のためにログへ出力する「Nordic BLE sniffer meta」ヘッダが含まれています
    • nRF Sniffer User Guide v1.1 (PDF) - www.nordicsemi.com Page 8 より
      4   Using Wireshark
      
      All BLE packets detected by the Sniffer are passed to Wireshark and are
      wrapped in a header which contains useful meta-information not present
      in the BLE packet itself. Wireshark dissects the packets and separates
      the actual packet from the meta-information.
                   :
      
  • 記事には下記資料からの抜粋へのリンクを参照用に適宜挿入しています
  • 随所に「https://developer.bluetooth.org/gatt/」下のサービス・キャラクタリスティックの記事へのリンクを張っていますが、2016年7月頃までは参照可能だった各記事が現在はリンク切れになっており(再構成中?)、さらに現時点では代替ページが見当たらないためリンク先はやむなく http://web.archive.org/ 上のキャッシュとしています。ただし web.archive.org 上にも保存履歴のない記事については一階層上のページへのリンクを張っています
    例:「Link Loss」サービスの単独ページのキャッシュは web.archive.org にも見当たらないため、上位のサービス一覧ページのキャッシュをターゲットに

局面

Scene 1: デバイス A のアドバタイジングと SCAN_REQ / SCAN_RSP

Frame 124 - 126 はデバイス A 発のアドバタイジングパケット

  • 全 40 チャネルのうちアドバタイジングパケット用の 37, 38, 39 の 3 チャネルが順次使用されていることが見てとれる
  • 「0x8e89bed6」の Access Address はアドバタイジングチャネルパケットで使用される固定値
  • デバイス A のアドバタイジングのタイプは Connectable Undirected(詳細: 1, 2, 3, 4) であることを示す一般的な「ADV_IND」である
  • 「Advertising Data: 020106020a000702031802180418」に含まれる不完全 16ビットサービスクラス UUID は次の内容
    • 16 bit uuid: 0x1803 --> 既定の「Link Loss」サービス
    • 16 bit uuid: 0x1802 --> 既定の「Immediate Alert」サービス
    • 16 bit uuid: 0x1804 --> 既定の「Tx Power」サービス
Frame 124

    

Frame 125

    

Frame 126

    

Frame 127

接続に際し対向アプリがデバイス A に対して SCAN_REQ を発行

    

Frame 128

デバイス A が対向アプリからの SCAN_REQ に対し SCAN_RSP を返信。ここでは ScanRspData としてデバイスのローカルネームが渡されている

    

Scene 2: 接続の確立と情報交換

Frame 217

対向アプリがデバイス A に対して CONNECT_REQ(詳細:1a, 2a, 1b, 2b)を発行して接続を試みる

  • 「Connection Request」中の「Connection Access Address」には、接続確立後のデータ通信において Access Address として双方が一意に使用する任意のアドレスが含まれる。ここでは「0xa6a188c8」
  • 「Connection Request」中の「Channel map」には接続確立後のデータ通信で使用するチャネル番号のリストが提示される。ここでは 0 - 36 の全データチャネルが指定されている
  • その他のパラメータについては上のリンクの資料を参照のこと
  • CONNECT_REQ に対する直接のレスポンスは発生しない。接続が確立するとデータ通信アイドル時にスレーブ - マスタ間で空パケット(Empty PDU)の応酬が始まるためこれが接続成否の判定に用いられる

    

Frame 218, 224 は接続確立後の最初の応酬。接続が確立すると対向アプリはマスタ、デバイス A はスレーブの位置づけとなる。ここでは リンク層制御 PDU の LL_FEATURE_REQ および LL_FEATURE_RSP によりマスタ - スレーブ間で Feature Exchange を行っている。ここでは双方とも「Supported feature: LE Encryption (0)」のみを提示(ただしここでは以降の通信において暗号化は行われていない)
Frame 218

    

Frame 224

    

Frame 222 ではアプリ側が「Device Name」を要求し Frame 226 でデバイス A 側がそれに応えている。
「Device Name」は、既定の Generic Access(GAP)サービス配下の既定のキャラクタリスティック(UUID = 0x2A00)であり、GATT サーバは必ず GAP サービスを含んでいる(詳細:1, 2
Frame 222

  Opcode: Read By Type Request (0x08)
  Starting Handle: 0x0001
  Ending Handle: 0xffff
  UUID: Device Name (0x2a00)

    

Frame 226

  Opcode: Read By Type Response (0x09)
  Length: 6
  Attribute Data, Handle: 0x0003
      Handle: 0x0003
      Value: ********

    

(GAP サービスおよび Device Name キャラクタリスティックは後続の Frame 231, Frame 266 であらためて表に現れる)

Scene 3: デバイス A の GATT サービス群の取得

(Scene 3, Scene 4 共通の基礎知識)
BLUETOOTH SPECIFICATION Version 4.2(PDF) [Vol 3, Part G] より
    2.6 GATT PROFILE HIERARCHY
       2.6.1 Overview, 2.6.2 Service, 2.6.3 Included Services, 2.6.4 Characteristic
  3 SERVICE INTEROPERABILITY REQUIREMENTS
     3.1 SERVICE DEFINITION, 3.2 INCLUDE DEFINITION, 3.3 CHARACTERISTIC DEFINITION
       3.3.1 Characteristic Declaration
         3.3.1.1 Characteristic Properties, 3.3.1.2 Characteristic Value Attribute Handle, 3.3.1.3 Characteristic UUID
       3.3.2 Characteristic Value Declaration

初期処理として、マスタはスレーブの提供するサービス一覧を取得する (詳細:1, 2, 3

  • アプリ側が、デバイス A の GATT サーバの提供する公開サービス(プライマリサービス)の問合せを開始。 以降、GATT サーバ上の所定のサービスへアクセスするためのハンドルのアドレスと当該サービスの種類を識別する UUID を順次取得する
  • ハンドルのアドレス空間は 0x0000 - 0xFFFF であり GATT サーバ側の応答に応じて照会範囲を絞っていく


      (図は 「BLUETOOTH SPECIFICATION Version 4.2」 より引用)

Frame 229

まずハンドル 0x0001 - 0xffff 全範囲についてプライマリサービスを照会

    Opcode: Read By Group Type Request (0x10)
    Starting Handle: 0x0001
    Ending Handle: 0xffff
    UUID: GATT Primary Service Declaration (0x2800)

    

Frame 231

以下のみっつのサービスの情報が得られた

  • ハンドルグループ 0x0001 - 0x000b:既定の「Generic Access」サービス(UUID = 0x1800)が使用
  • ハンドルグループ 0x000c - 0x000f:既定の「Generic Attribute」サービス(UUID = 0x1801)が使用
  • ハンドルグループ 0x0010 - 0x0022:既定の「Device Information」サービス(UUID = 0x180A)が使用

    Opcode: Read By Group Type Response (0x11)
    Length: 6
    Attribute Data, Handle: 0x0001, Group End Handle: 0x000b
        Handle: 0x0001
        Group End Handle: 0x000b
        Value: 0018
    Attribute Data, Handle: 0x000c, Group End Handle: 0x000f
        Handle: 0x000c
        Group End Handle: 0x000f
        Value: 0118
    Attribute Data, Handle: 0x0010, Group End Handle: 0x0022
        Handle: 0x0010
        Group End Handle: 0x0022
        Value: 0a18

    

Frame 235

続けてハンドル 0x0023 - 0xffff 範囲のプライマリサービスを照会

    

Frame 238

以下のみっつのサービスの情報が得られた

  • ハンドルグループ 0x0023 - 0x0025:既定の「Immediate Alert」サービス(UUID = 0x1802)が使用
  • ハンドルグループ 0x0026 - 0x002a:既定の「Tx Power」サービス(UUID = 0x1804)が使用
  • ハンドルグループ 0x002b - 0x002d:既定の「Link Loss」サービス(UUID = 0x1803)が使用

    

Frame 242

続けてハンドル 0x002e - 0xffff 範囲のプライマリサービスを照会

    

Frame 247

以下のみっつのサービスの情報が得られた

  • ハンドルグループ 0x002e - 0x0032:既定の「Battery Service」サービス(UUID = 0x180F)が使用
  • ハンドルグループ 0x0033 - 0x0037:ユーザ定義のサービス 1(UUID = 0xFFE0)が使用
  • ハンドルグループ 0x0038 - 0x003a:ユーザ定義のサービス 2(UUID = 0xFFF0)が使用

    

Frame 250

続けてハンドル 0x003b - 0xffff 範囲のプライマリサービスを照会

    

Frame 253

「Attribute Not Found (0x0a)」のエラーが返る。これ以上プライマリサービスが存在しないことを意味する。サービスの照会はここまで

    Opcode: Error Response (0x01)
    Request Opcode in Error: Read By Group Type Request (0x10)
    Handle in Error: 0x003b
    Error Code: Attribute Not Found (0x0a)

    

Scene 4: デバイス A 各サービス配下のキャラクタリスティック - デスクリプタの取得

(Scene 3, Scene 4 共通の基礎知識)
BLUETOOTH SPECIFICATION Version 4.2(PDF) [Vol 3, Part G] より
    2.6 GATT PROFILE HIERARCHY
       2.6.1 Overview, 2.6.2 Service, 2.6.3 Included Services, 2.6.4 Characteristic
  3 SERVICE INTEROPERABILITY REQUIREMENTS
     3.1 SERVICE DEFINITION, 3.2 INCLUDE DEFINITION, 3.3 CHARACTERISTIC DEFINITION
       3.3.1 Characteristic Declaration
         3.3.1.1 Characteristic Properties, 3.3.1.2 Characteristic Value Attribute Handle, 3.3.1.3 Characteristic UUID
       3.3.2 Characteristic Value Declaration

次に、収集ずみの各プライマリサービスのハンドルグループごとに以下を行う (詳細:1, 2, 3, 4, 5, 6

  • 所定のサービスに含まれる Included Service の照会


      (図は 「BLUETOOTH SPECIFICATION Version 4.2」 より引用)

  • 所定のサービスに含まれるキャラクタリスティックの照会


      (図は 「BLUETOOTH SPECIFICATION Version 4.2」 より引用)

    • 所定のキャラクタリスティックに含まれるデスクリプタの照会


        (図は 「BLUETOOTH SPECIFICATION Version 4.2」 より引用)

Frame 255

前掲の Frame 231 の示すハンドルグループ 0x0001 - 0x000b の「Generic Access」サービス内の Included Service を照会

    Opcode: Read By Type Request (0x08)
    Starting Handle: 0x0001
    Ending Handle: 0x000b
    UUID: GATT Include Declaration (0x2802)

    

Frame 259

当該サービス内に Included Service は存在しない(注:図のログではスニファがパケットデータを取りこぼしている)

    

Frame 262

ハンドルグループ 0x0001 - 0x000b の「Generic Access」サービス内のキャラクタリスティックを照会

    Opcode: Read By Type Request (0x08)
    Starting Handle: 0x0001
    Ending Handle: 0x000b
    UUID: GATT Characteristic Declaration (0x2803)

    

Frame 266

以下のみっつのキャラクタリスティックの情報が得られた

(※キャラクタリスティック の Value フィールドの構成は前掲の「BLUETOOTH SPECIFICATION Version 4.2] - [Vol 3, Part G] p.532 「3.3.1 Characteristic Declaration」 に、プロパティ値の意味は同じく p.533 「3.3.1.1 Characteristic Properties」 に説明あり)

  • ハンドル:0x0002
    「value: 080300002a」より、プロパティ = 0x08 (Write), Characteristic Value Handle = 0x0003
    UUID = 0x2A00 = 既定の「Device Name
  • ハンドル:0x0004
    「value: 020500012a」より、プロパティ = 0x02 (Read), Characteristic Value Handle = 0x0005
    UUID = 0x2A01 = 既定の「Appearance
  • ハンドル:0x0006
    「value: 020700042a」より、プロパティ = 0x02 (Read), Characteristic Value Handle = 0x0007
    UUID = 0x2A04 = 既定の「Peripheral Preferred Connection Parameters

    Opcode: Read By Type Response (0x09)
    Length: 7
    Attribute Data, Handle: 0x0002
      Handle: 0x0002
      Value: 080300002a
    Attribute Data, Handle: 0x0004
      Handle: 0x0004
      Value: 020500012a
    Attribute Data, Handle: 0x0006
      Handle: 0x0006
      Value: 020700042a

    

Frame 270

続けてハンドル 0x0007 - 0x000b 範囲のキャラクタリスティックを照会

    

Frame 275

以下のふたつのキャラクタリスティックの情報が得られた

  • ハンドル:0x0008
    「value: 020900022a」より、プロパティ = 0x02 (Read), Characteristic Value Handle = 0x0009
    UUID = 0x2A02 = 既定の「Peripheral Privacy Flag
  • ハンドル:0x000a
    「value: 020b00032a」より、プロパティ = 0x02 (Read), Characteristic Value Handle = 0x000b
    UUID = 0x2A03 = 既定の「Reconnection Address

    

Frame 279

前掲の Frame 231 の示すハンドルグループ 0x000c - 0x000f の「Generic Attribute」サービス内の Included Service を照会

    

Frame 284

当該サービス内に Included Service は存在しない

    

Frame 287

ハンドルグループ 0x000c - 0x000f の「Generic Attribute」サービス内のキャラクタリスティックを照会

    

Frame 289

以下のキャラクタリスティックの情報が得られた

  • ハンドル:0x000d
    「value: 200e00052a」より、プロパティ = 0x20 (Indicate), Characteristic Value Handle = 0x000e
    UUID = 0x2A52 = 既定の「Service Changed

    

Frame 293

続けてハンドル 0x000e - 0x000f 範囲のキャラクタリスティックを照会

    

Frame 298

当該サービス内にはこれ以上キャラクタリスティックは存在しない

    

Frame 302

Frame 289 の示す通り 0x0000e は Service Changed キャラクタリスティックの Value Handle であり、残る0x000f に関する情報を GATT に問い合わせてみる (詳細:1, 2

    Opcode: Find Information Request (0x04)
    Starting Handle: 0x000f
    Ending Handle: 0x000f

    

Frame 305

0x000f は Service Changed キャラクタリスティックの Client Characteristic Configuration Descriptor (UUID = 0x2902) (詳細:1, 2) である旨の情報が得られた

    Opcode: Find Information Response (0x05)
    UUID Format: 16-bit UUIDs (0x01)
    Handle: 0x000f
    UUID: Client Characteristic Configuration (0x2902)

    

Frame 308

前掲の Frame 231 の示すハンドルグループ 0x0010 - 0x0022 の「Device Information」サービス内の Included Service を照会

    

Frame 312

当該サービス内に Included Service は存在しない

    

Frame 315

ハンドルグループ 0x0010 - 0x0022 の「Device Information」サービス内のキャラクタリスティックを照会

    

Frame 319

以下のみっつのキャラクタリスティックの情報が得られた

  • ハンドル:0x00011
    「value: 021200292a」より、プロパティ = 0x02 (Read), Characteristic Value Handle = 0x0012
    UUID = 0x2A29 = 既定の「Manufacturer Name String
  • ハンドル:0x00013
    「value: 021400242a」より、プロパティ = 0x02 (Read), Characteristic Value Handle = 0x0014
    UUID = 0x2A24 = 既定の「Model Number String
  • ハンドル:0x00015
    「value: 021600252a」より、プロパティ = 0x02 (Read), Characteristic Value Handle = 0x0014
    UUID = 0x2A25 = 既定の「Serial Number String

    

Frame 323

続けてハンドル 0x0016 - 0x0022 範囲のキャラクタリスティックを照会

    

Frame 326

以下のみっつのキャラクタリスティックの情報が得られた

  • ハンドル:0x00017
    「value: 021800262a」より、プロパティ = 0x02 (Read), Characteristic Value Handle = 0x0018
    UUID = 0x2A26 = 既定の「Firmware Revision String
  • ハンドル:0x00019
    「value: 021a00272a」より、プロパティ = 0x02 (Read), Characteristic Value Handle = 0x001a
    UUID = 0x2A27 = 既定の「Hardware Revision String
  • ハンドル:0x0001b
    「value: 021c00282a」より、プロパティ = 0x02 (Read), Characteristic Value Handle = 0x001c
    UUID = 0x2A28 = 既定の「Software Revision String

    

Frame 330

続けてハンドル 0x001c - 0x0022 範囲のキャラクタリスティックを照会

    

Frame 333

以下のみっつのキャラクタリスティックの情報が得られた

  • ハンドル:0x0001d
    「value: 021e00232a」より、プロパティ = 0x02 (Read), Characteristic Value Handle = 0x001e
    UUID = 0x2A23 = 既定の「System ID
  • ハンドル:0x0001f
    「value: 0220002a2a」より、プロパティ = 0x02 (Read), Characteristic Value Handle = 0x0020
    UUID = 0x2A2A = 既定の「IEEE 11073-20601 Regulatory Certification Data List
  • ハンドル:0x00021
    「value: 022200502a」より、プロパティ = 0x02 (Read), Characteristic Value Handle = 0x0022
    UUID = 0x2A50 = 既定の「PnP ID

    

Frame 337

前掲の Frame 238 の示すハンドルグループ 0x0023 - 0x0025 の「Immediate Alert」サービス内の Included Service を照会
(注:このリクエストに対する正しいレスポンスは「Attribute Not Found (0x0a)」だが、スニファ取りこぼしのためログが欠落している)

    

Frame 341

ハンドルグループ 0x0023 - 0x0025 の「Immediate Alert」サービス内のキャラクタリスティックを照会

    

Frame 345

以下のキャラクタリスティックの情報が得られた

  • ハンドル:0x0024
    「value: 042500062a」より、
    プロパティ = 0x04 (Write without Response),
    Characteristic Value Handle = 0x0025
    UUID = 0x2A06 = 既定の「Alert Level

    

Frame 349

前掲の Frame 238 の示すハンドルグループ 0x0026 - 0x002a の「Tx Power」サービス内の Included Service を照会
(注:このリクエストに対する正しいレスポンスは「Attribute Not Found (0x0a)」だが、スニファ取りこぼしのためログが欠落している)

    

Frame 355

ハンドルグループ 0x0026 - 0x002a の「Tx Power」サービス内のキャラクタリスティックを照会

    

Frame 359

以下のキャラクタリスティックの情報が得られた

  • ハンドル:0x0027
    「value: 122800072a」より、プロパティ = 0x12 (Read | Notify), Characteristic Value Handle = 0x0028
    UUID = 0x2A07 = 既定の「Tx Power Level

    

Frame 362

続けてハンドル 0x0028 - 0x002a 範囲のキャラクタリスティックを照会

    

Frame 366

当該サービス内にはこれ以上キャラクタリスティックは存在しない

    

Frame 369

Frame 359 の示す通り 0x00028 は Tx Power Level キャラクタリスティックの Value Handle であり後続の 0x0029 - 0x002a に関する情報を GATT に問い合わせてみる (詳細:1, 2

    

Frame 373

0x0029 は Tx Power Level キャラクタリスティックの Client Characteristic Configuration Descriptor (UUID = 0x2902) (詳細:1, 2) である旨の情報が得られた

    

Frame 376

残る0x002a に関する情報を GATT に問い合わせてみる (詳細:1, 2

    

Frame 380

0x002a は Tx Power Level キャラクタリスティックの Characteristic Presentation Format Descriptor (UUID = 0x2904) (詳細:1, 2, 3, 4) である旨の情報が得られた

    Opcode: Find Information Response (0x05)
    UUID Format: 16-bit UUIDs (0x01)
    Handle: 0x002a
    UUID: Characteristic Presentation Format (0x2904)

    

Frame 384

前掲の Frame 238 の示すハンドルグループ 0x002b - 0x002d の「Link Loss」サービス内の Included Service を照会
(注:このリクエストに対する正しいレスポンスは「Attribute Not Found (0x0a)」だが、スニファ取りこぼしのためログが欠落している)

    

Frame 390

ハンドルグループ 0x002b - 0x002d の「Link Loss」サービス内のキャラクタリスティックを照会

    

Frame 392

以下のキャラクタリスティックの情報が得られた

  • ハンドル:0x002c
    「value: 0a2d00062a」より、プロパティ = 0x0a (Read | Write), Characteristic Value Handle = 0x002d
    UUID = 0x2A06 = 既定の「Alert Level

    

Frame 395

前掲の Frame 247 の示すハンドルグループ 0x002e - 0x0032 の「Battery Service」サービス内の Included Service を照会

    

Frame 397

当該サービス内に Included Service は存在しない

    

Frame 401

ハンドルグループ 0x002e - 0x0032 の「Battery Service」サービス内のキャラクタリスティックを照会

    

Frame 404

以下のキャラクタリスティックの情報が得られた

  • ハンドル:0x002f
    「value: 123000192a」より、プロパティ = 0x12 (Read | Notify), Characteristic Value Handle = 0x0030
    UUID = 0x2A19 = 既定の「Battery Level

    

Frame 407

続けてハンドル 0x0030 - 0x0032 範囲のキャラクタリスティックを照会

    

Frame 411

当該サービス内にこれ以上キャラクタリスティックは存在しない

    

Frame 415

Frame 404 の示す通り 0x00030 は Battery Level キャラクタリスティックの Value Handle であり後続の 0x0031 - 0x0032 に関する情報を GATT に問い合わせてみる (詳細:1, 2

    

Frame 420

0x0031 は Battery Level キャラクタリスティックの Client Characteristic Configuration Descriptor (UUID = 0x2902) (詳細:1, 2) である旨の情報が得られた

    

Frame 423

残る0x0032 に関する情報を GATT に問い合わせてみる (詳細:1, 2

    

Frame 427

0x0032 は Battery Level キャラクタリスティックの Characteristic Presentation Format Descriptor (UUID = 0x2904) (詳細:1, 2, 3, 4) である旨の情報が得られた

    

Frame 431

前掲の Frame 247 の示すハンドルグループ 0x0033 - 0x0037 のユーザ定義サービス 1(UUID = 0xFFE0)内の Included Service を照会

    

Frame 434

当該サービス内に Included Service は存在しない

    

Frame 437

ハンドルグループ 0x0033 - 0x0037 のユーザ定義サービス 1 内のキャラクタリスティックを照会

    

Frame 439

以下のキャラクタリスティックの情報が得られた

  • ハンドル:0x0034
    「value: 103500e1ff」より、
    プロパティ = 0x10 (Notify),
    Characteristic Value Handle = 0x0035
    UUID = 0xFFE1 : ユーザ定義のキャラクタリスティック

    

Frame 442

続けてハンドル 0x0035 - 0x0037 範囲のキャラクタリスティックを照会

    

Frame 446

当該サービス内にこれ以上キャラクタリスティックは存在しない

    

Frame 450

Frame 439 の示す通り 0x00035 はユーザ定義キャラクタリスティック(UUID = 0xFFE1)の Value Handle であり後続の 0x0036 - 0x0037 に関する情報を GATT に問い合わせてみる (詳細:1, 2

    

Frame 453

0x0036 はユーザ定義キャラクタリスティック(UUID = 0xFFE1)の Client Characteristic Configuration Descriptor (UUID = 0x2902) (詳細:1, 2) である旨の情報が得られた

    

Frame 456

残る0x0037 に関する情報を GATT に問い合わせてみる (詳細:1, 2

    

Frame 458

0x0037 はユーザ定義キャラクタリスティック(UUID = 0xFFE1)の Characteristic User Description Descriptor (UUID = 0x2901)である旨の情報が得られた

    

Frame 462

前掲の Frame 247 の示すハンドルグループ 0x0038- 0x003a のユーザ定義サービス 2(UUID = 0xFFF0)内の Included Service を照会

    

Frame 465

当該サービス内に Included Service は存在しない

    

Frame 468

ハンドルグループ 0x0038 - 0x003a のユーザ定義サービス 2 内のキャラクタリスティックを照会

    

Frame 470

以下のキャラクタリスティックの情報が得られた

  • ハンドル:0x0039
    「value: 043a00f1ff」より、プロパティ = 0x04 (Write without Response), Characteristic Value Handle = 0x003a
    UUID = 0xFFF1 : ユーザ定義のキャラクタリスティック

    

Scene 5: デバイス A ボタン押下時のアプリへの通知を設定

Frame 491

Frame 453Frame 439Frame 453 の示す通り、ハンドル 0x0036 は、 "ユーザ定義サービス 1(UUID = 0xFFE0)配下のユーザ定義キャラクタリスティック(UUID = 0xFFE1)配下の Client Characteristic Configuration Descriptor (CCCD)" である

当該ユーザ定義キャラクタリスティック(UUID = 0xFFE1)のプロパティには Notify が設定されており、クライアントである対向アプリから CCCD 0x0036 に Notification bit (0x0001) を書き込んでおくことで、このキャラクタリスティックの値が更新された時に Value Handle である 0x0035 経由でアプリ側へ通知(Notification)が行われるようになる
(詳細: 1a, 2a, 1b, 2b

    Opcode: Write Request (0x12)
    Handle: 0x0026
    Value: 0100

    

Frame 495

    Opcode: Write Response (0x13)

    

Scene 6: アプリからデバイス A の LED・ブザーを操作

Frame 616

対向アプリ上の所定のボタンを押下すると、Frame 345, Frame 238 の示す、 "「Immediate Alert」サービス配下の「Alert Level」キャラクタリスティック" の Value Handle 0x0025 に値「2(High Alert)」が書き込まれる

デバイス A の LED・ブザーはファームウェアレベルでこの Alert Level キャラクタリスティックに紐づけられている模様。High Alert 値が書き込まれるとそれに反応して短時間 LED とブザーが ON になる

なお、Frame 345 の示すように Alert Level キャラクタリスティックのプロパティには「Write without Response」(0x04) が設定されているため、アプリから値を書き込んでもデバイス A からのレスポンスは発生しない

    Opcode: Write Command (0x52)
    Handle: 0x0025
    Value: 02

    

Scene 7: デバイス A のボタンを押すとアプリへ通知

Frame 711

デバイス A の物理ボタンはファームウェアレベルで Frame 439 の示す "ユーザ定義サービス 1(UUID = 0xFFE0)配下のユーザ定義キャラクタリスティック(UUID = 0xFFE1)" に紐づけられている模様。このボタンを押すと当該キャラクタリスティックの値がデバイス内部で更新され Frame 491 での仕込みに基づきアプリ側へ通知が行われる

    Opcode: Handle Value Notification (0x1b)
    Handle: 0x0035
    Value: 01

    

Scene 8: アプリ側からデバイス A との接続を切断

対向アプリ上の所定のボタンを押下するとリンク層制御 PDU の LL_TERMINATE_IND (0x02) がデバイス A 側に送出され両者間の接続が終了する(詳細:1, 2

Frame 779

    


(tanabe)
klab_gijutsu2 at 19:02|この記事のURLComments(0)TrackBack(0)
2016年08月18日

技適マークつき BLE パケットスニファを入手する

はてなブックマークに登録

Bluetooth Low Energy (BLE) の勉強のために BLE パケットを覗いてみたいと思いました。BLE の通信プロトコルは複雑ですが、パケットの内容を適宜精査すれば座学的な情報の向こう側にある実像を捉えることが可能となるでしょう。

国内では次のような BLE プロトコルアナライザが販売されています。もっともこういった数百万円オーダーの専用機にはなかなか手を出せません。

もっと手軽な方法として、BLE チップ・モジュールベンダの提供するパケットスニファを利用する選択があります。代表的な製品をピックアップしてみます。 2016年8月時点では日本国内で正規に流通している BLE パケットスニファ製品は見当たりません。もちろん国外から調達することは可能ですが、電波法に基づく技術基準適合(技適)証明とのかねあいが気になるところです。

パケットスニファと技適

たとえば、前掲の Nordic Semiconductor 社製「nRF51 Dongle」は技適証明を受けていないためディストリビュータが次のように注意を促しています。

  • nRF51 USB dongle for emulator,firmware - jp.rs-online.com
    警告
    
    本開発キットは技術基準適合証明を受けておりません。本製品のご使用に際しては、
    電波法遵守のため、以下のいずれかの措置を取っていただく必要がありますので
    ご注意ください。
    
     - 電波法施行規則第 6 条第 1 項第 1 号に基づく平成 18 年 3 月 28 日総務省告示
       第 173 号で定められた電波暗室等の試験設備内で使用する。
     - 実験局の免許を取得したのち使用する。
     - 技術基準適合証明を取得したのち使用する。
    
ただ、電波法は受信のみを目的とするものを規制対象外としています。
  • 電波法 (最終改正:平成二七年五月二二日法律第二六号) - law.e-gov.go.jp
       第一章 総則
              :
    第二条 この法律及びこの法律に基づく命令の規定の解釈に関しては、次の定義に
           従うものとする。 
              :
      五  「無線局」とは、無線設備及び無線設備の操作を行う者の総体をいう。
           但し、受信のみを目的とするものを含まない。 
              :
    
機能の性質上、パケットスニファの通信上の役割は受信に特化しています。ではこのデバイスを規制対象外と判断し安心して国内で使うことは適切でしょうか? 実はさらに考慮すべき話題があります。

上の記事のように、nRF51 Dongle は mbed 対応のプログラマブルな無線通信デバイスです。つまり、この「無線設備」は元来「受信のみを目的とするもの」ではなくむしろ「操作を行う者」によってプログラムを書き換え可能であることを特長のひとつに掲げている製品です。上に挙げたスニファデバイスはいずれも同様の側面を持っています。

以下の例のように、技適マークつきの製品においてさえ「ファームウェアの書き換え」との整合性を一意に判断できない事情を考え合わせると、技適マークなしのこれらのスニファ製品の日本国内での使用の是非はやはり微妙かもしれません。

  • ESP-WROOM-02のファームウェアを書き換えた場合、技適はどうなるのか - スイッチサイエンス
    ユーザーによるファームウェアの書き換えが、ESP-WROOM-02の工事設計認証を
    無効にする可能性について、メーカーのEspressif Systemsに確認をしました。
    同社は登録認証機関に確認した上で、Arduino core for ESP8266 WiFi chip
    または同社製SDKを使っている限りにおいては、認証には影響を与えないという
    回答を下さいました。他の開発環境など、ファームウェアを書き換える部分に
    よっては、認証に影響を及ぼし得るとのことですので、ご注意ください。
    
  • モノワイヤレス製品情報 - MONO-WIRELESS.COM - mono-wireless.com
    電波法規(技適)について
             :
    更にファームウエアを書き換えると認証の範囲を外れてしまう無線モジュール
    も存在しています。弊社製品は全て技適認証に適合した無線モジュールですので
    コンプライアンスに背くことなく安心してご使用していただけます。併せて
    「電波法についての考慮事項」を参照ください。
    
  • DD-WRT - wikipedia
    電波法による規制
    
    日本においては無線機器に対してメーカー側が想定していない非公式ファーム
    ウェアへの書き換えを行った時点で技適マークが無効となり、無線LANを利用する
    場合は電波法に違反する[要出典]。
             :
    

Adafruit 社製「Bluefruit LE Sniffer」

そんなわけで前掲のスニファの導入は一旦保留していましたが、情報を探しているうちにふとある製品のスペックに目がとまりました。

MDBT40 は、Nordic Semiconductor 社製のメジャーな nRF51822 チップを搭載した 中国 Raytac 社の BLE モジュールです。前に BLE まわりの製品調査を行っていた折にこの MDBT40 が日本の技適証明を取得ずみであることを知り名前が印象に残っていました。

MDBT40 を載せた製品は下記の例のように国内で流通しています。

  • Adafruit Feather 32u4 Bluefruit LE - スイッチサイエンス
    BLE(Bluetooth Low Energy)機能付きの小型Arduino互換ボードです。
    Adafruit Bluefruit LE Microの後継品です。
                    :
    Arduino Leonardoなどに搭載されているATmega 32u4を搭載。
    BLEモジュールであるMDBT40は、総務省の工事設計認証(いわゆる技適)を
    取得済みなので、日本国内で使用することができます。
                    :
    
Adafruit 社製のこの「Bluefruit LE Sniffer」の製品写真をよく見ると、技適マークつきの MDBT40 が載っていることを確認できます。 (クリックで大きく表示)

このスニファであれば技適関連のジレンマなしに利用できそうです。本体価格は直販で $29.95。以下、Adafruit 社公式サイトより。

Bluefruit LE Sniffer のポイントをざっくりまとめてみます。

さっそく Adafruit 社サイトでオーダーしました。 配送方法には最も安価な United States Postal Service の「First-Class Package International Service™ incl. $1.60 insurance : $16.40」を指定、計 $46.35 を PayPal で決済し一週間ほどで到着しました。

発送連絡後に USPS サイトで配送状況を確認すると経由地に「JAMAICA」の表記あり。さては Japan -> Jamaica のミスか? と疑いましたがこれはカリブ海域にある国の名前ではなく JFK 空港そばの「JAMAICA, NY 11430」でした。物流の要衝のようですが恥ずかしながらニューヨークにジャマイカという地名があることを初めて知りました。Adafruit さん疑ってごめんなさい(^^;

使ってみる

Windows PC で Bluefruit LE Sniffer を使用する最短の手順を示します。詳細は nRF Sniffer アーカイブ内の「nRF Sniffer User Guide」に記載されています。

  1. FTDI 社の仮想 COM ポートドライバを未導入ならインストール
  2. 最新の nRF Sniffer の zip アーカイブをダウンロードし適当なフォルダ A へ展開
  3. Wireshark(v1.10.1 以降)を未導入ならインストール
  4. Bluefruit LE Sniffer を USB ポートへ接続し Sniffing の対象とするデバイスを近接させておく
    図は「nRF Sniffer User Guide v1.2」より
  5. フォルダ A 直下の「ble-sniffer_win_<version>_Sniffer.exe」を実行
  6. コンソールが開きアドバタイジング中の BLE デバイスが一覧表示される
  7. カーソルキー+ENTER または「#」番号で対象とするデバイスを選択
  8. デバイスを選択した状態で「w」キーを押下すると Wireshark が起動。あとはデバイス側で必要な操作を行えばよい

付記:デュアルユース品と輸出規制

ご存知の方も多いと思いますが、MouserRSDigi-key といった一般のディストリビュータから所定の電子部品を購入しようとすると、その製品の内容と在庫元・出荷元の国や地域によっては軍事転用の可能な「デュアルユース品目」として輸出規制に引っかかり、所定の書類一式の提出を求められ審査のために数週間程度待たされる場合があります。

今回、某ディストリビュータへ所定の商品が輸出規制の対象が否かを事前に判別する方法の有無を尋ねたところ「注文を受け実際に輸出手続きを開始しなければわからない」との回答でした。 輸出規制に関する注意書きの有無はまちまちですが、以下に一例を引用します。

  • nRF51-Dongle Nordic Semiconductor | Mouser - www.mouser.jp
    This product may require additional documentation to export
    from the United States.
    この製品をアメリカから配送するには、追加の書類が必要になる
    ことがあります。
    
一方、直販も行っている Adafruit は自社製品の輸出管理を主体的に実施している旨を公式サイト上で宣言しています。こういう場合には直販を利用するほうが面倒が少ないようです。


(tanabe)
klab_gijutsu2 at 11:35|この記事のURLComments(4)TrackBack(0)
2016年08月03日

Tomoru と Pochiru と Linking

はてなブックマークに登録

Tomoru

Tomoru は、昨年(2015年)11月より国内のクラウドファンディングサービス Makuake で支援募集の行われた Bluetooth Low Energy(BLE)デバイスです。「Project Linking」の立ち上げとともに当時話題になったためご存知の方も多いことでしょう。2016年8月現在は製造元である株式会社 Braveridge公式ストアAmazon から入手することができます。以前このブログで「blink(1)」という製品をピックアップしたことがありますが、個人的に「光りもの」は好物で、この Tomoru も支援募集への応募によって届いた個体を持っています。

Tomoru

www.products.braveridge.com

現時点では対応アプリが全体的にやや大人しめで Linking 自体も本格的な普及はこれからのようですが、手元では BLE の勉強がてらにこの可愛いデバイスを楽しんでいます。

Pochiru

Braveridge 社はその後も活発に Linking 対応製品をリリースしています。最近目にとまった「Pochiru」というデバイスを買ってみました。Pochiru には LED に加えネーミングどおり押しボタンが実装されています。そのため通信先の機器との双方向のやりとりが可能です。

Pochiru

www.products.braveridge.com
ボタンものの BLE デバイスはいろいろ販売されていますが、リモートシャッターやメディアプレイヤーの操作といった所定の目的に閉じたものが多く、廉価でありながら自作のアプリケーションと汎用的に連携可能であることが Pochiru の魅力です。この価格でこのサイズの BLE ボタンデバイスを自作することの難度を考えるとコストパフォーマンスの高い製品と言えるでしょう。

プログラムを書く

Tomoru や Pochiru のような Linking 対応デバイス用のアプリは Project Linking が無償で公開している SDK を利用して作成します。BLE まわりのハンドリングはすべて Linking 本体が仲介するため、抽象化された Linking API を呼び出すだけでデバイスとの連携が可能です。

Project Linking の開発者向けページの中ほどの「API guide」項の利用規約を確認の上 SDK をダウンロードし、「API 仕様書」の「ポーティング」ページの説明に添って開発環境をセットアップします。後は「API 仕様書」と SDK アーカイブに含まれる「LinkingIFDemo」のコードを読み合わせれば要領を理解しやすいでしょう。

手元では Android 版 SDK を利用しています。上記のサンプルを元に作成した簡単なアプリのコードを以下に示します。アプリの「LED」ボタンを押すと連携ずみデバイスの LED が点灯し、連携ずみデバイス側のボタン押下等のイベントによってアプリが通知を受けるとその旨を画面に表示する内容です。

試作:Linking01

MainActivity.java
/**
 *
 * Linking01
 *
 * - 連携ずみ Linking デバイスの LED を点灯させる
 * - Linking デバイスからの通知を受け取る
 *
 */

package jp.klab.Linking01;

import android.content.Context;
import android.content.SharedPreferences;
import android.media.AudioManager;
import android.media.ToneGenerator;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import com.nttdocomo.android.sdaiflib.Define;
import com.nttdocomo.android.sdaiflib.NotifyNotification;
import com.nttdocomo.android.sdaiflib.SendOther;

public class MainActivity extends AppCompatActivity
        implements View.OnClickListener {

    private Context mCtx;
    private Button mButtonLED;

    private static final byte LINKING_IF_PATTERN_ID = 0x20; //LEDパターンの設定項目ID(固定値)
    private static final byte LINKING_IF_COLOR_ID = 0x30;   //LED色の設定項目ID(固定値)
    private static final byte COLOR_ID_RED = 0x01;  // 点灯色
    private static final byte BLINK_PATTERN = 0x22; // 点灯パターン

    private NotifyNotification mNotifyNotification;
    private MyNotificationInterface mMyNotificationInterface;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mCtx = this;
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        mButtonLED = (Button)findViewById(R.id.buttonLED);
        mButtonLED.setOnClickListener(this);
        // Linking デバイスからの通知受信用
        mMyNotificationInterface = new MyNotificationInterface();
        mNotifyNotification = new NotifyNotification(this, mMyNotificationInterface);
    }

    @Override
    public void onClick(View v) {
        // LED ボタン押下で連携ずみデバイスあてに LED 点灯指示を送る
        if (v == (View)mButtonLED) {
            SendOther sendOther = new SendOther(this);
            sendOther.setIllumination(
                    new byte[] {
                            LINKING_IF_PATTERN_ID,
                            BLINK_PATTERN,
                            LINKING_IF_COLOR_ID,
                            COLOR_ID_RED
                    });
            sendOther.send();
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mNotifyNotification.release();
    }

    private class MyNotificationInterface implements NotifyNotification.NotificationInterface {
        @Override
        public void onNotify() { // 通知を受信した
            // Linking デバイスからの通知内容は SharedPreferences に記録される
            SharedPreferences preference =
                    getSharedPreferences(Define.NotificationInfo, Context.MODE_PRIVATE);
            int DEVICE_ID = preference.getInt("DEVICE_ID", -1);
            int DEVICE_BUTTON_ID = preference.getInt("DEVICE_BUTTON_ID", -1);
            // Toast 表示
            Toast.makeText(mCtx, "onNotify: DEVICE_ID=" + DEVICE_ID +
                    " DEVICE_BUTTON_ID=" + DEVICE_BUTTON_ID, Toast.LENGTH_SHORT).show();
            // 音も鳴らす
            ToneGenerator toneGenerator
                           = new ToneGenerator(AudioManager.STREAM_SYSTEM, ToneGenerator.MAX_VOLUME);
            toneGenerator.startTone(ToneGenerator.TONE_CDMA_CALL_SIGNAL_ISDN_PING_RING);
        }
    }
}

試作の動作の様子

(動画:36秒 音なし)

Linking でのデバイス・アプリの設定手順

Linking 対応デバイスと Linking アプリを連携させるためには事前に設定が必要です。手順は以下の要領です。

  1. 端末に「Linking」をインストールする(未インストールの場合のみ)

             Linking(Android 版)- play.google.com

  2. 連携させるアプリ(ここでは自作の「Linking01」)のインストールを行い Linking を実行する
      
  3. 「デバイスの検索」を行い連携対象のデバイスを Linking に接続する(当該デバイスを未登録の場合のみ)
            
         
  4. デバイスの登録・接続が完了したら「デバイス詳細画面」へ移動し当該デバイスと連携させるアプリケーション(複数可。ここでは「Linking01」)を設定する
            
  5. Linking を抜け上の手順で設定したアプリを起動してデバイスとの連携を確認する
         

備考: デバイスの Linking への再接続について

手元の Android 5.1 端末の環境では以下の現象が見られます。Linking のバージョンは 2016年8月現在最新の「03.10.00000」です。

  • Linking に接続ずみのデバイスを Linking 上で「切断」すると再接続できなくなる
               
手元ではこの状況に以下の手順で対処しています。
  • Android の Bluetooth 設定において当該デバイスとのペアを解除してから Linking 上で再接続を行う
               
         
関連記事
(tanabe)
klab_gijutsu2 at 19:54|この記事のURLComments(2)
Blog内検索
Archives
このブログについて
DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
最新コメント