AndroidをBLEペリフェラルにしよう

AndroidのAPIを使ってアプリを作成してスマホをBLEペリフェラルにして、PCのブラウザからBLEでつないでみます。
Androidのスマホには、豊富なハードウェアやネットワーク機能がありますので、これがあれば、同じWiFiアクセスポイントにつながなくても、PCのブラウザからBLEでつないでみることができます。

ソースコードもろもろは以下に上げました。

https://github.com/poruruba/BlePeripheralTest

# Android

(i) BLEアダプタの取得

```java
        mBleManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
        mBleAdapter = mBleManager.getAdapter();
```

(ii) GATTサーバの作成

```java
        mBtGattServer = mBleManager.openGattServer(this, mGattServerCallback);
```

mGattServerCallback は、BLEセントラルからの接続が完了したときや、ATT Read/Write が来た時の処理を実装するためのコールバックです。後で説明します。


(iii) GATT構造の作成

PrimaryServiceの下に、3つのキャラクタリスティックを配置します。

・Write用
・Read用
・Notification用

```java
	/* GATT構造(PrimaryService) */
        btGattService = new BluetoothGattService(UUID_LIFF_SERVICE, BluetoothGattService.SERVICE_TYPE_PRIMARY);

	/* GATT構造(Characteristic) */
        mBtCharacteristic1 = new BluetoothGattCharacteristic(UUID_LIFF_WRITE, BluetoothGattCharacteristic.PROPERTY_WRITE, BluetoothGattCharacteristic.PERMISSION_WRITE);
        btGattService.addCharacteristic(mBtCharacteristic1);
        mBtCharacteristic2 = new BluetoothGattCharacteristic(UUID_LIFF_READ, BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_READ);
        btGattService.addCharacteristic(mBtCharacteristic2);
        mNotifyCharacteristic = new BluetoothGattCharacteristic(UUID_LIFF_NOTIFY, BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattCharacteristic.PERMISSION_READ);
        btGattService.addCharacteristic(mNotifyCharacteristic);
        BluetoothGattDescriptor dataDescriptor = new BluetoothGattDescriptor(UUID_LIFF_DESC, BluetoothGattDescriptor.PERMISSION_WRITE | BluetoothGattDescriptor.PERMISSION_READ);
        mNotifyCharacteristic.addDescriptor(dataDescriptor);
        mBtGattServer.addService(btGattService);
```

(iv) アドバタイジングデータの作成

PrimaryServiceのUUIDをサービスUUIDとしてアドバタイジングデータに含めています。そうすることで、BLEセントラル側からBLEペリフェラルをScanする際に検索対象を絞ることができます。
あと、BLEセントラルからわかりやすいように、デバイス名も含めています。

```java
        AdvertiseData.Builder dataBuilder = new AdvertiseData.Builder();
        dataBuilder.setIncludeTxPowerLevel(true);
        dataBuilder.addServiceUuid(ParcelUuid.fromString(UUID_LIFF_SERVICE_STR));

        AdvertiseSettings.Builder settingsBuilder = new AdvertiseSettings.Builder();
        settingsBuilder.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED);
        settingsBuilder.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM);
        settingsBuilder.setTimeout(0);
        settingsBuilder.setConnectable(true);

        AdvertiseData.Builder respBuilder = new AdvertiseData.Builder();
        respBuilder.setIncludeDeviceName(true);
```

(v) アドバタイズの開始

```java
        mBtAdvertiser.startAdvertising(settingsBuilder.build(), dataBuilder.build(), respBuilder.build(), new AdvertiseCallback(){
            @Override
            public void onStartSuccess(AdvertiseSettings settingsInEffect) {
                Log.d("bleperi", "onStartSuccess");
            }
            @Override
            public void onStartFailure(int errorCode) {
                Log.d("bleperi", "onStartFailure");
                handler.sendTextMessage("BLEを開始できませんでした。");
            }
        });
```

(vi) コールバック関数

BLEの状態が変わったり、BLEセントラルからのリクエストを受け取った時に呼び出される関数を実装しています。
ここが一番重要なところです。

```java
    private BluetoothGattServerCallback mGattServerCallback = new BluetoothGattServerCallback() {
```

主なものとして以下があります。

・public void onMtuChanged (BluetoothDevice device, int mtu){

　クライアントからの要求により、MTUサイズが変更されたときに呼び出されます。MTUサイズが必要な場合はここで取得した値を覚えておくようにします。
　ブラウザのWeb Bluetooth APIからは、512バイトに拡大されました。NordicのnRF Connect for Mobileは拡大されず20バイトずつの送受信となりました。クライアント(BLEセントラル)によって違うようです。

・public void onConnectionStateChange(android.bluetooth.BluetoothDevice device, int status, int newState) {

　BLEセントラルと接続されたり切断されたときに呼び出されます。ですが、最新のAndroidOSバージョンでは、誰も接続していないはずなのに、接続状態となります。謎です。
　接続されたときに、android.bluetooth.BluetoothDevice というインスタンスが渡されます。以降これを使うので、値を覚えておきます。

・public void onCharacteristicReadRequest(android.bluetooth.BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) {

　BLEセントラルから、ATT Readが呼び出されたときに、呼び出されます。要は、読み出し要求です。
　内部のバッファに、受信しておいたデータを返してあげましょう。
　ポイントは以下の部分です。

```java
        if( offset > charValue.length ) {
            mBtGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, offset, null);
        }else {
            byte[] value = new byte[charValue.length - offset];
            System.arraycopy(charValue, offset, value, 0, value.length);
            mBtGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value);
        }
```

BLEセントラルとBLEペリフェラルの間での1回のBLEパケットの長さは、MTUサイズです。この長さを超えて送受信するには、複数のパケットを投げる必要があります。
offsetは、どこを起点として読み出すかをBLEセントラル側が指定します。BLEセントラル側は、BLEペリフェラルから受信したデータ長がMTUサイズ以下か、エラーが返ってくるまで、offsetを進めながらATT Readを繰り返し呼び出します。それに耐えられるような実装にしたつもりです。

・public void onCharacteristicWriteRequest(android.bluetooth.BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {

　BLEセントラルから、ATT Writeが呼び出されたときに、呼び出されます。要は、書き込み要求です。
　内部のバッファに、受信データを保存しましょう。BLEセントラル側からoffsetを使って分割してATT Writeされる場合があるので、offsetが内部保持可能なバッファサイズを超えたらエラーを返しています。

```java
        if(offset < charValue.length ) {
            int len = value.length;
            if( (offset + len ) > charValue.length)
                len = charValue.length - offset;
            System.arraycopy(value, 0, charValue, offset, len);
            mBtGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null);
        }else {
            mBtGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, offset, null);
        }
```

・public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {

　主に、CCCD(Client Characteristic Configuration Descriptor)の操作に使います。
　CCCDは2バイトです。受信データの1バイト目が1だったら、Notificationが有効で、0だったら無効ですので、Notification送信時に確認するようにします。

・public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattDescriptor descriptor) {

　主に、CCCD(Client Characteristic Configuration Descriptor)の値の読み出しに使います。

(vii) その他

* 各種UUIDを使っていますが、適当に払い出したものなので、UUIDは各自払い出してください。
* Manifestには以下の許可が必要だと思います。

```xml
    <uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>

    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
```

ちなみに、画面はこんな感じ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/7d2a9582-0f78-e8f4-4b13-e5e56d2f5776.png)

# クライアント側（ブラウザ）からつなぐ

動作確認用に、Webページを作ってみました。
HTML5に、Web Bluetooth APIがあり、ChromeからBLEペリフェラルと通信することができるので、それを使いました。
Windows10で確認しましたが、iMacやAndroidでも動くはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/0590acbc-c8e2-83e9-c8ec-b771192a4706.png)

まずは、「接続」ボタンを押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/2182878f-608d-13a5-10a2-a339a4429d7f.png)

そうすると、設定したPrimaryServiceのUUIDを持ったサービスがリストアップされます。
いづれかを選択して、「ペア設定」を押下すると、BLE接続処理が走ります。Notificationも有効にしています。
接続が完了すると、connected のところが、 true になっているかと思います。

あとは、write valueのテキストエリアところに、16進数文字列を指定して、「Write」ボタンを押下すると、ATT Writeが実行され、「Read」ボタンを押下すると、ATT Readが実行されます。
ちなみに、write valueの先頭1バイトが0xFFの場合には、Notificationが発行されるようにAndroid側を実装しています。
ATT Readで受信したデータはread valueのテキストエリアに、Notificationで受信したデータはnotify valueのテキストエリアに表示するようにしています。

HTMLファイルです。

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval'; style-src * 'unsafe-inline'; media-src *; img-src * data: content:;">
    <meta name="format-detection" content="telephone=no">
    <meta name="msapplication-tap-highlight" content="no">
    <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">

  <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
  <script src="https://code.jquery.com/jquery-1.12.4.min.js" integrity="sha384-nvAa0+6Qg9clwYCGGPpDQLVpLNn0fRaROjHqs13t4Ggj3Ez50XnGQqc/r8MhnRDZ" crossorigin="anonymous"></script>
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
  <!-- Optional theme -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap-theme.min.css" integrity="sha384-6pzBo3FDv/PJ8r2KRkGHifhEocL+1X2rVCTTkUfGk7/0pbek5mMa1upzvWbrUbOZ" crossorigin="anonymous">
  <!-- Latest compiled and minified JavaScript -->
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js" integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd" crossorigin="anonymous"></script>

  <title>BLE Peripheral Test</title>

  <script src="js/methods_utils.js"></script>
  <script src="js/vue_utils.js"></script>

  <script src="dist/js/vconsole.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
    <div id="top" class="container">
        <h1>BLE Peripheral Test</h1>
        <label>connected</label> {{ble_connected}}<br>
        <label>deviceName</label> {{ble_devicename}}<br>
        <button class="btn btn-default" v-on:click="ble_connect">接続</button>
        <br><br>
        <label>write value</label>
        <textarea type="string" rows="5" class="form-control" v-model="ble_write_value"></textarea>
        <button class="btn btn-default" v-on:click="ble_write">Write</button>
        <br><br>
        <button class="btn btn-default" v-on:click="ble_read">Read</button>
        <br>
        <label>read value</label>
        <textarea type="string" rows="5" class="form-control" v-model="ble_read_value" readonly></textarea>
        <br>
        <label>notify value</label>
        <textarea type="string" rows="5" class="form-control" v-model="ble_notify_value" readonly></textarea>



        <div class="modal fade" id="progress">
            <div class="modal-dialog">
                <div class="modal-content">
                    <div class="modal-header">
                        <h4 class="modal-title">{{progress_title}}</h4>
                    </div>
                    <div class="modal-body">
                        <center><progress max="100" /></center>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script src="js/start.js"></script>
</body>
</html>
```

Javascriptです。
UUIDは、Androidで払い出したUUIDに合わせてください。


```js:start.js
'use strict';

//var vConsole = new VConsole();

const UUID_ANDROID_SERVICE = 'a9d158bb-9007-4fe3-b5d2-d3696a3eb067';
const UUID_ANDROID_WRITE = '52dc2801-7e98-4fc2-908a-66161b5959b0';
const UUID_ANDROID_READ = '52dc2802-7e98-4fc2-908a-66161b5959b0';
const UUID_ANDROID_NOTIFY = '52dc2803-7e98-4fc2-908a-66161b5959b0';

const ANDROID_WAIT = 200;

var bluetoothDevice = null;
var characteristics = new Map();

var vue_options = {
    el: "#top",
    data: {
        progress_title: '',

        ble_connected: false,
        ble_devicename: '', 
        ble_write_value: '',
        ble_read_value: '',
        ble_notify_value: '',
    },
    computed: {
    },
    methods: {
        ble_connect: function(){
            return this.requestDevice(UUID_ANDROID_SERVICE)
            .then( (name) => {
                this.progress_open();
                this.ble_devicename = name;
                return bluetoothDevice.gatt.connect()
                .then(server => {
                    console.log('Execute : getPrimaryService');
                    return wait_async(ANDROID_WAIT)
                    .then(() =>{
                        return server.getPrimaryService(UUID_ANDROID_SERVICE);
                    });
                })
                .then(service => {
                    console.log('Execute : getCharacteristic');
                    characteristics.clear();
                    return Promise.all([
                        this.setCharacteristic(service, UUID_ANDROID_WRITE),
                        this.setCharacteristic(service, UUID_ANDROID_READ),
                        this.setCharacteristic(service, UUID_ANDROID_NOTIFY)
                    ]);
                })
                .then(values => {
                    return wait_async(ANDROID_WAIT)
                    .then(() =>{
                        return this.startNotify(UUID_ANDROID_NOTIFY);
                    });
                })
                .then(() =>{
                    this.ble_connected = true;
                    console.log('ble_connect done');
                    return bluetoothDevice.name;
                })
                .catch(error =>{
                    alert(error);
                })
                .finally(() => {
                    this.progress_close();
                });
            })
            .catch(error =>{
                alert(error);
            });
        },
        ble_read: function(){
            return this.readChar(UUID_ANDROID_READ);
        },
        ble_write: function(){
            return this.writeChar(UUID_ANDROID_WRITE, hexs2bytes(this.ble_write_value, ''));
        },
        requestDevice: function(service_uuid){
            console.log('Execute : requestDevice');
    
            return navigator.bluetooth.requestDevice({
                filters: [{services:[ service_uuid ]}]
        //		acceptAllDevices: true,
        //		optionalServices: [service_uuid]
                }
            )
            .then(device => {
                console.log("requestDevice OK");
                characteristics.clear();
                bluetoothDevice = device;
                bluetoothDevice.addEventListener('gattserverdisconnected', this.onDisconnect);
                return bluetoothDevice.name;
            });
        },
        setCharacteristic: function(service, characteristicUuid) {
            console.log('Execute : setCharacteristic : ' + characteristicUuid);

            return wait_async(ANDROID_WAIT)
            .then(() => {
                return service.getCharacteristic(characteristicUuid);
            })
            .then( (characteristic) =>{
                characteristics.set(characteristicUuid, characteristic);
                characteristic.addEventListener('characteristicvaluechanged', this.onDataChanged);
                return service;
            });
        },
        onDisconnect: function(event){
            console.log('onDisconnect');
            characteristics.clear();
            this.ble_connected = false;
        },
        onDataChanged: function(event){
            console.log('onDataChanged');

            let characteristic = event.target;
            let packet = uint8array_to_array(characteristic.value);
            if( characteristic.uuid == UUID_ANDROID_READ ){
                this.ble_read_value = bytes2hexs(packet, '');
            }else if( characteristic.uuid == UUID_ANDROID_NOTIFY ){
                this.ble_notify_value = bytes2hexs(packet, '');
            }
        },    
        startNotify: function(uuid) {
            if( characteristics.get(uuid) === undefined )
                throw "Not Connected";
    
            console.log('Execute : startNotifications');
            return characteristics.get(uuid).startNotifications();
        },
        stopNotify: function(uuid){
            if( characteristics.get(uuid) === undefined )
                throw "Not Connected";
    
            console.log('Execute : stopNotifications');
            return characteristics.get(uuid).stopNotifications();
        },
        writeChar: function(uuid, array_value) {
            if( characteristics.get(uuid) === undefined )
                throw "Not Connected";
    
            console.log('Execute : writeValue');
            let data = Uint8Array.from(array_value);
            return characteristics.get(uuid).writeValue(data);
        },
        readChar: function(uuid){
            if( characteristics.get(uuid) === undefined )
                throw "Not Connected";
    
            console.log('Execute : readValue');
            return characteristics.get(uuid).readValue((dataView) =>{
                console.log(dataView);
            });
        }
    },
    created: function(){
        proc_load();

        var ary = [];
        for( var i = 0 ; i < 500 ; i++ )
            ary[i] = i & 0xff;
        this.ble_write_value = bytes2hexs(ary, '');
    }
};
vue_add_methods(vue_options, methods_utils);
var vue = new Vue( vue_options );

function hexs2bytes(hexs, sep = ' ') {
    hexs = hexs.trim(hexs);
    if( sep == '' )
    {
        hexs = hexs.replace(/ /g, "");
        var array = [];
        for( var i = 0 ; i < hexs.length / 2 ; i++)
            array[i] = parseInt(hexs.substr(i * 2, 2), 16);
        return array;
    }else{
        return hexs.split(sep).map(function(h) { return parseInt(h, 16) });
    }
}

function bytes2hexs(bytes, sep = ' ') {
    return bytes.map(function(b) { var s = b.toString(16); return b < 0x10 ? '0'+s : s; }).join(sep).toUpperCase();
}

function uint8array_to_array(array)
{
	var result = new Array(array.byteLength);
	var i;
	for( i = 0 ; i < array.byteLength ; i++ )
		result[i] = array.getUint8(i);
		
	return result;
}

function wait_async(timeout){
	return new Promise((resolve, reject) =>{
		setTimeout(resolve, timeout);
	});
}
```

足りないソースファイルは以下にあります。

　https://github.com/poruruba/swagger_template/tree/master/public 

主要な関数について解説します。

〇ble_connect
　BLEデバイスに接続し、PrimaryServiceやCharacteristicを検索します。また、Notificationも有効化します。
　HTML5のWeb Bluetooth APIを使っているため、本Webページは、HTTPSでホスティングされている必要があります。
　（随所にウェイトをいれていますが、なくても大丈夫かもしれません。）

〇ble_write
　ATT Writeの関数です。
　MTUサイズに合わせて、勝手に分割送信してくれているようです。ですが、最大でも512バイトだそうです。

〇ble_read
　ATT Readの関数です。ただし、読み出したデータは、 onDataChanged がコールバックされて取得できます。

〇onDataChanged
　ATT Readしたときに読みだしが完了したときと、Notificationを受信したときに呼び出されます。

# 参考情報

・Android Deveopers Reference
https://developer.android.com/reference/android/bluetooth/package-summary 
https://developer.android.com/reference/android/bluetooth/le/package-summary 

・Web Bluetoot
https://tkybpp.github.io/web-bluetooth-jp/ 

## (参考) Android側のソースコードです。長いです。

```java:MainActivity.java
package com.example.test.bleperi.bleperipheraltest;

import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattServer;
import android.bluetooth.BluetoothGattServerCallback;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.le.AdvertiseCallback;
import android.bluetooth.le.AdvertiseData;
import android.bluetooth.le.AdvertiseSettings;
import android.bluetooth.le.BluetoothLeAdvertiser;
import android.content.Context;
import android.os.Message;
import android.os.ParcelUuid;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;
import android.widget.Toast;
import java.util.UUID;
import static com.example.test.bleperi.bleperipheraltest.UIHandler.MSG_ID_OBJ_BASE;
import static com.example.test.bleperi.bleperipheraltest.UIHandler.MSG_ID_TEXT;

public class MainActivity extends Activity implements UIHandler.Callback {
    UIHandler handler;
    BluetoothManager mBleManager;
    BluetoothAdapter mBleAdapter;
    BluetoothLeAdvertiser mBtAdvertiser;
    BluetoothGattCharacteristic mPsdiCharacteristic;
    BluetoothGattCharacteristic mBtCharacteristic1;
    BluetoothGattCharacteristic mBtCharacteristic2;
    BluetoothGattCharacteristic mNotifyCharacteristic;
    BluetoothGattService btPsdiService;
    BluetoothGattService btGattService;
    BluetoothGattServer mBtGattServer;
    boolean mIsConnected = false;
    BluetoothDevice mConnectedDevice;

//    private static final short WIRELESS_BLE_MAX_L2CAP_SIZE = 23;

    private static final UUID UUID_LIFF_PSDI_SERVICE = UUID.fromString("e625601e-9e55-4597-a598-76018a0d293d");
    private static final UUID UUID_LIFF_PSDI = UUID.fromString("26e2b12b-85f0-4f3f-9fdd-91d114270e6e");
    private static final String UUID_LIFF_SERVICE_STR = "a9d158bb-9007-4fe3-b5d2-d3696a3eb067";

    private static final UUID UUID_LIFF_SERVICE = UUID.fromString(UUID_LIFF_SERVICE_STR);
    private static final UUID UUID_LIFF_WRITE = UUID.fromString("52dc2801-7e98-4fc2-908a-66161b5959b0");
    private static final UUID UUID_LIFF_READ = UUID.fromString("52dc2802-7e98-4fc2-908a-66161b5959b0");
    private static final UUID UUID_LIFF_NOTIFY = UUID.fromString("52dc2803-7e98-4fc2-908a-66161b5959b0");
    private static final UUID UUID_LIFF_DESC = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");

    private static final int UUID_LIFF_VALUE_SIZE = 500;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        handler = new UIHandler(this);

        mBleManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
        mBleAdapter = mBleManager.getAdapter();

        if(mBleAdapter != null){
            prepareBle();
        }
    }

    @Override
    public boolean handleMessage(Message message) {
        switch (message.what) {
            case MSG_ID_TEXT: {
                TextView txt;
                txt = (TextView) findViewById(R.id.txt_message);
                txt.setText((String) message.obj);
                return true;
            }
            case MSG_ID_OBJ_BASE: {
                if( message.arg1 == 1 ){
                    TextView txt;
                    txt = (TextView) findViewById(message.arg2);
                    txt.setText((String)message.obj);
                }
                break;
            }
        }
        return false;
    }

    private void prepareBle(){
        mBtAdvertiser = mBleAdapter.getBluetoothLeAdvertiser();
        if( mBtAdvertiser == null ){
            Toast.makeText(this, "BLE Peripheralモードが使用できません。", Toast.LENGTH_SHORT).show();
            return;
        }

        mBtGattServer = mBleManager.openGattServer(this, mGattServerCallback);

        btPsdiService = new BluetoothGattService(UUID_LIFF_PSDI_SERVICE, BluetoothGattService.SERVICE_TYPE_PRIMARY);
        mPsdiCharacteristic = new BluetoothGattCharacteristic(UUID_LIFF_PSDI, BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_READ);
        btPsdiService.addCharacteristic(mPsdiCharacteristic);
        mBtGattServer.addService(btPsdiService);

        try { Thread.sleep(200); }catch(Exception ex){}

        btGattService = new BluetoothGattService(UUID_LIFF_SERVICE, BluetoothGattService.SERVICE_TYPE_PRIMARY);

        mBtCharacteristic1 = new BluetoothGattCharacteristic(UUID_LIFF_WRITE, BluetoothGattCharacteristic.PROPERTY_WRITE, BluetoothGattCharacteristic.PERMISSION_WRITE);
        btGattService.addCharacteristic(mBtCharacteristic1);
        mBtCharacteristic2 = new BluetoothGattCharacteristic(UUID_LIFF_READ, BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_READ);
        btGattService.addCharacteristic(mBtCharacteristic2);
        mNotifyCharacteristic = new BluetoothGattCharacteristic(UUID_LIFF_NOTIFY, BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattCharacteristic.PERMISSION_READ);
        btGattService.addCharacteristic(mNotifyCharacteristic);
        BluetoothGattDescriptor dataDescriptor = new BluetoothGattDescriptor(UUID_LIFF_DESC, BluetoothGattDescriptor.PERMISSION_WRITE | BluetoothGattDescriptor.PERMISSION_READ);
        mNotifyCharacteristic.addDescriptor(dataDescriptor);
        mBtGattServer.addService(btGattService);

        try { Thread.sleep(200); }catch(Exception ex){}

        startBleAdvertising();
    }

    private void startBleAdvertising(){
        AdvertiseData.Builder dataBuilder = new AdvertiseData.Builder();
        dataBuilder.setIncludeTxPowerLevel(true);
        dataBuilder.addServiceUuid(ParcelUuid.fromString(UUID_LIFF_SERVICE_STR));

        AdvertiseSettings.Builder settingsBuilder = new AdvertiseSettings.Builder();
        settingsBuilder.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED);
        settingsBuilder.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM);
        settingsBuilder.setTimeout(0);
        settingsBuilder.setConnectable(true);

        AdvertiseData.Builder respBuilder = new AdvertiseData.Builder();
        respBuilder.setIncludeDeviceName(true);

        mBtAdvertiser.startAdvertising(settingsBuilder.build(), dataBuilder.build(), respBuilder.build(), new AdvertiseCallback(){
            @Override
            public void onStartSuccess(AdvertiseSettings settingsInEffect) {
                Log.d("bleperi", "onStartSuccess");
            }
            @Override
            public void onStartFailure(int errorCode) {
                Log.d("bleperi", "onStartFailure");
                handler.sendTextMessage("BLEを開始できませんでした。");
            }
        });
    }

    private BluetoothGattServerCallback mGattServerCallback = new BluetoothGattServerCallback() {
        private byte[] psdiValue = new byte[8];
        private byte[] notifyDescValue = new byte[2];
        private byte[] charValue = new byte[UUID_LIFF_VALUE_SIZE]; /* max 512 */

        @Override
        public void onMtuChanged (BluetoothDevice device, int mtu){
            Log.d("bleperi", "onMtuChanged(" + mtu + ")");
        }

        @Override
        public void onConnectionStateChange(android.bluetooth.BluetoothDevice device, int status, int newState) {
            Log.d("bleperi", "onConnectionStateChange");

            if(newState == BluetoothProfile.STATE_CONNECTED){
                mConnectedDevice = device;
                mIsConnected = true;
                Log.d("bleperi", "STATE_CONNECTED:" + device.toString());
                handler.sendTextMessage("接続されました。");
                handler.sendUIMessage(MSG_ID_OBJ_BASE, 1, R.id.txt_target_address, device.getAddress());
            }
            else{
                mIsConnected = false;
                Log.d("bleperi", "Unknown STATE:" + newState);
                handler.sendTextMessage("切断されました。");
                handler.sendUIMessage(MSG_ID_OBJ_BASE, 1, R.id.txt_target_address, "");
            }
        }

        public void onCharacteristicReadRequest(android.bluetooth.BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) {
            Log.d("bleperi", "onCharacteristicReadRequest");

            if( characteristic.getUuid().compareTo(UUID_LIFF_PSDI) == 0) {
                mBtGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, psdiValue);
            }else if( characteristic.getUuid().compareTo(UUID_LIFF_READ) == 0){
                handler.sendUIMessage(MSG_ID_OBJ_BASE, 1, R.id.txt_access, "Read");
                handler.sendUIMessage(MSG_ID_OBJ_BASE, 1, R.id.txt_offset, Integer.toString(offset));
                handler.sendUIMessage(MSG_ID_OBJ_BASE, 1, R.id.txt_length, "");
                handler.sendUIMessage(MSG_ID_OBJ_BASE, 1, R.id.txt_value, "");

                if( offset > charValue.length ) {
                    mBtGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, offset, null);
                }else {
                    byte[] value = new byte[charValue.length - offset];
                    System.arraycopy(charValue, offset, value, 0, value.length);
                    mBtGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value);
                }
            }else{
                mBtGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, offset, null );
            }
        }

        public void onCharacteristicWriteRequest(android.bluetooth.BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
            Log.d("bleperi", "onCharacteristicWriteRequest");

            if( characteristic.getUuid().compareTo(UUID_LIFF_WRITE) == 0 ){
                handler.sendUIMessage(MSG_ID_OBJ_BASE, 1, R.id.txt_access, "Write");
                handler.sendUIMessage(MSG_ID_OBJ_BASE, 1, R.id.txt_offset, Integer.toString(offset));
                handler.sendUIMessage(MSG_ID_OBJ_BASE, 1, R.id.txt_length, Integer.toString(value.length));
                handler.sendUIMessage(MSG_ID_OBJ_BASE, 1, R.id.txt_value, MainActivity.toHexString(value));

                if(offset < charValue.length ) {
                    int len = value.length;
                    if( (offset + len ) > charValue.length)
                        len = charValue.length - offset;
                    System.arraycopy(value, 0, charValue, offset, len);
                    mBtGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null);
                }else {
                    mBtGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, offset, null);
                }

                if( (notifyDescValue[0] & 0x01) != 0x00 ) {
                    if (offset == 0 && value[0] == (byte) 0xff) {
                        mNotifyCharacteristic.setValue(charValue);
                        mBtGattServer.notifyCharacteristicChanged(mConnectedDevice, mNotifyCharacteristic, false);
                        handler.sendTextMessage("Notificationしました。");
                    }
                }
            }else{
                mBtGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, offset, null);
            }
        }

        public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattDescriptor descriptor) {
            Log.d("bleperi", "onDescriptorReadRequest");

            if( descriptor.getUuid().compareTo(UUID_LIFF_DESC) == 0 ) {
                mBtGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, notifyDescValue);
            }
        }

        public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
            Log.d("bleperi", "onDescriptorWriteRequest");

            if( descriptor.getUuid().compareTo(UUID_LIFF_DESC) == 0 ) {
                notifyDescValue[0] = value[0];
                notifyDescValue[1] = value[1];

                mBtGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null);
            }
        }
    };

    public static String toHexString(byte[] data) {
        StringBuffer sb = new StringBuffer();
        for (byte b : data) {
            String s = Integer.toHexString(0xff & b);
            if (s.length() == 1)
                sb.append("0");
            sb.append(s);
        }
        return sb.toString();
    }
}
```

# あとがき

投稿したはいいけど、読者には結構BLEの知識が必要かも。。。
実験したいことがあればお知らせください。

以上