BLEを使ってスマホとPCのブラウザでファイル共有

超久しぶりの投稿です。
今回は、BLEを使ってAndroidやiOSのスマホと、PC側のブラウザを使ってファイルの共有をします。
スマホにはCordovaを使ったネイティブアプリをインストールする必要があります。
スマホのネイティブアプリは、AndroidとiOSで動作を確認しました。

通信手段はBLEを使うので、PC側の事前の準備は不要です。Bluetoothドングルを買っておくぐらい。
また、ブラウザのWebBluetooth APIを使っているので、インストールは不要です。なので、PCじゃなくてもスマホのブラウザでも動作します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/c5ac222d-00e6-4780-866a-47ae79bf674f.png)

ソースコード一式は以下のGitHubにあります。

poruruba/BleDataShareTool

https://github.com/poruruba/BleDataShareTool

また、ブラウザ側のページは以下からでもアクセスできます。

https://poruruba.github.io/BleDataShareTool/client/bledatashare/


# スマホ側

## 利用するCordovaプラグイン

スマホ側は、マルチプラットフォームであるCordovaを使います。
BLE Peripheralとして立ち上げるため、プラグイン「cordova-plugin-ble-peripheral」を使います。その他、プラグインを追加います。

* cordova-plugin-ble-peripheral
BLEペリフェラルとしてふるまうために利用します。
★のはずだったのですが、エラーが出ていたので修正しておきました。以下にあります。

github:poruruba/cordova-plugin-ble-peripheral

https://github.com/poruruba/cordova-plugin-ble-peripheral

* cordova.plugins.diagnostic
BLE利用のための権限確認が実行時に行う必要があります。

* cordova-plugin-device
iOSかAndroidかを区別するために利用します。

* cordova-plugin-file
対向と共有したファイルをスマホに保存するために利用します。

以上のプラグインを利用するため、あとはJavascriptの実装のみです。

## サービスUUID・キャラクタリスティックUUID

UUIDはそれぞれ適当に以下を利用します。

<pre>
var SERVICE_UUID = 'a9d158bb-9007-4fe3-b5d2-d3696a3eb067';
var TX_UUID = '52dc2801-7e98-4fc2-908a-66161b5959b0';
var RX_UUID = '52dc2802-7e98-4fc2-908a-66161b5959b0';
</pre>

SERVICE_UUIDがサービスUUIDで、BLEセントラルからBLEペリフェラルを探すときに使います。
TX_UUIDは書き込み用のキャラクタリスティックです。スマホからPC側にデータ送信する際に使います。送信する内容として2つの用途があります。

・受信したいファイルのオフセットを書き込む
・スマホ側に送信したいファイルを書き込む

RX_UUIDは読み込み用＋通知用の共用です。読み込みはPC側からの共有するファイルを受信するために使います。また、スマホ側からPC側に送信する場合にスマホ側で次のオフセットからのデータ受信の準備ができたことを通知するためにも使います。

・送信したいファイルのオフセット以降のデータを配置
・送信したいファイルのオフセット以降のデータが準備できたことを通知

簡単に送信フローを以下にまとめます。PC側からスマホに送信したい場合と、スマホからPC側に送信したい場合の2パターンがあります。

スマホからPCへの送信

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/f7679b00-5964-1b37-889d-98df2a871142.png)

PCからスマホへの送信

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/eb76bee2-92f8-26e8-5f76-bab07cbab803.png)

## BLEペリフェラルの立ち上げ

以下を呼び出します。

```js:js/start.js
                    blePeripheral.onWriteRequest(this.didReceiveWriteRequest);
                    this.createService();

        createService: function() {
            // https://learn.adafruit.com/introducing-the-adafruit-bluefruit-le-uart-friend/uart-service
            // Characteristic names are assigned from the point of view of the Central device
    
            var property = blePeripheral.properties;
            var permission = blePeripheral.permissions;
    
            Promise.all([
                blePeripheral.createService(SERVICE_UUID),
                blePeripheral.addCharacteristic(SERVICE_UUID, TX_UUID, property.WRITE, permission.WRITEABLE),
                blePeripheral.addCharacteristic(SERVICE_UUID, RX_UUID, property.READ | property.NOTIFY, permission.READABLE),
                blePeripheral.publishService(SERVICE_UUID),
                blePeripheral.startAdvertising(SERVICE_UUID)
            ]).then(
                function() { console.log ('Created Service'); },
            );
        },
```

上記を呼び出す前に、BLEの権限承認が必要であるため、以下の権限承認フローの中で、呼び出します。

```js:js/start.js
        request_permissions: async function(){
            if( device.platform == 'Android'){
                cordova.plugins.diagnostic.isBluetoothAvailable((available) =>{
                    console.log("Bluetooth is " + (available ? "available" : "not available"));
                    if( !available ){
                        alert("Bluetoothが有効になっていません。");
                        return;
                    }
                }, (error) =>{
                    console.error("The following error occurred: "+error);
                });
            }
            var permissions = ["BLUETOOTH_ADVERTISE", "BLUETOOTH_CONNECT"];
            cordova.plugins.diagnostic.getBluetoothAuthorizationStatuses((statuses) =>{
                console.log(statuses);
                if( statuses[permissions[0]] != "GRANTED" || statuses[permissions[1] != "GRANTED"] ){
                    cordova.plugins.diagnostic.requestBluetoothAuthorization(() =>{
                        console.log("Bluetooth authorization was requested.");
                        blePeripheral.onWriteRequest(this.didReceiveWriteRequest);
                        this.createService();
                    }, (error) =>{
                        console.error(error);
                    }, permissions);
                }else{
                    blePeripheral.onWriteRequest(this.didReceiveWriteRequest);
                    this.createService();
                }
            }, (error) =>{
                console.error(error);
            });
        },
```

## BLE受信処理

スマホ側は、TX_UUIDに書き込まれたことを契機に動作します。

スマホからPCへの送信の要求(OPERATION.READ)の場合には、受信したオフセットからのデータをRX_UUIDにセットして、自動的に通知が送信されるので、PC側からのRX_UUIDを受信することを期待します。それにより、PC側はそれを繰り返して受信データを結合して完了させます。

ＰＣからスマホへの送信の要求(OPERATION.WRITE)の場合には、受信したオフセットとデータ本体が受信されているので、それを覚えておきます。もしデータをすべて受信した場合には全受信データを結合して完了します。

```js:js/start.js
        didReceiveWriteRequest: async function(request) {
            var array = new Uint8Array(request.value);
            var operation = array[0];
            var offset = get_uint32b(array, 1);
            console.log("operation=" + operation + " offset: " + offset + " length=" + array.length);

            if( operation == OPERATION.READ ){
                var whole = new Uint8Array(1 + 4 + (read_data_array.length - offset));
                whole[0] = OPERATION.READ;
                set_uint32b(whole, offset, 1);
                whole.set( read_data_array.slice(offset), 1 + 4);
                await blePeripheral.setCharacteristicValue(SERVICE_UUID, RX_UUID, whole.buffer);
                console.log("setCharacteristicValue");
            }else
            if( operation == OPERATION.WRITE ){
                if( offset == 0 ){
                    write_data_length = get_uint32b(array, 1 + 4);
                    write_data_array = new Uint8Array(4 + write_data_length + 2);
                }
                write_data_array.set(array.slice(1 + 4), offset);
                offset += array.length - (1 + 4);
                if( offset >= (4 + write_data_length + 2) ){
                    var checksum = make_checksum(write_data_array, 0, 4 + write_data_length);
                    if( checksum != get_uint16b(write_data_array, 4 + write_data_length) ){
                        var whole = make_operation_array(OPERATION.ERROR, 0);
                        await blePeripheral.setCharacteristicValue(SERVICE_UUID, RX_UUID, whole.buffer);
                        console.log("setCharacteristicValue");
                        return;
                    }else{
                        var whole = make_operation_array(OPERATION.COMPLETE, offset);
                        await blePeripheral.setCharacteristicValue(SERVICE_UUID, RX_UUID, whole.buffer);
                        console.log("setCharacteristicValue");

                        this.read_raw = write_data_array.slice(4, 4 + write_data_length);
                        this.read_received_date = new Date().getTime();
                        this.parse_read();
                        this.toast_show("データを取得しました。");
                    }
                }else{
                    var whole = make_operation_array(OPERATION.WRITE, offset);
                    await blePeripheral.setCharacteristicValue(SERVICE_UUID, RX_UUID, whole.buffer);
                    console.log("setCharacteristicValue");
                }
            }
        },
```

## 受信データのパース

受信データは、以下の3つのいずれかのフォーマットになっています。

テキスト
type(1)=TEXT | text(n)

バイナリ
type(1)=BINARY | binary(n)

ファイル
type(1)=FILE | length_of_binary(4)=b | binary(b) | length_of_name(2)=n | name(n) | length_of_mimetype(1)=m | mimetype(m)

以下の部分でパースしています。

```js:js/start.js
        parse_read: async function(){
            this.read_type = this.read_raw[0];
            switch(this.read_type){
                case TYPE.BINARY:{
                    this.read_binary = this.ba2hex(this.read_raw.slice(1), '');
                    break;
                }
                case TYPE.TEXT:{
                    this.read_text = text_decoder.decode(this.read_raw.slice(1));
                    break;
                }
                case TYPE.FILE:{
                    var binary_length = get_uint32b(this.read_raw, 1);
                    var name_length = get_uint16b(this.read_raw, 1 + 4 + binary_length);
                    var mime_length = this.read_raw[1 + 4 + binary_length + 2 + name_length];
                    var read_file = {};
                    read_file.file_name = text_decoder.decode(this.read_raw.slice(1 + 4 + binary_length + 2, 1 + 4 + binary_length + 2 + name_length));
                    read_file.file_mimetype = text_decoder.decode(this.read_raw.slice(1 + 4 + binary_length + 2 + name_length + 1, 1 + 4 + binary_length + 2 + name_length + 1 + mime_length));
                    read_file.binary = this.read_raw.slice(1 + 4, 1 + 4 + binary_length);
                    read_file.file_size = read_file.binary.length;
                    this.read_file = read_file;
                    break;
                }
            }
        },
```

# PC側

PC側は、ほぼスマホ側の動作と同じですが、WebBluetooth APIを使っているところが違うくらいです。
Javascriptの処理のみです。WebBluetooth APIを使っているため、HTTPSにページをホスティングするか直接ファイルとしてブラウザにロードする必要があります。

データの送信、受信のいずれも、PC側からのTX_UUIDへの書き込みから始まります。

以下が、PC側が受信したい場合です。

```js:js/start.js
        do_ble_read: async function(){
            try{
                this.progress_open();
                read_data_buffers = [];
                read_data_length = 0;
                var whole = make_operation_array(OPERATION.READ, 0);
                await this.writeChar(UUID_ANDROID_WRITE, whole);
            }catch(error){
                this.progress_close();
                console.error(error);
                alert(error);
            }
        },
```

以下がPC側からデータを送信したい場合です。
1点注意ですが、一度のスマホ側に送信できるサイズはブラウザの実装に依存しますが、だいたい512バイトのようです。ですので、512バイトごとに区切って送る必要があります。

```js:js/start.js
        do_ble_write: async function(){
            try{
                this.progress_open();
                var whole = new Uint8Array(1 + 4 + write_data_array.length);
                whole[0] = OPERATION.WRITE;
                set_uint32b(whole, 0, 1);
                whole.set(write_data_array, 1 + 4);
                if( write_data_array.length > BLE_MTU_SIZE)
                    return this.writeChar(UUID_ANDROID_WRITE, whole.slice(0, BLE_MTU_SIZE));
                else
                    return this.writeChar(UUID_ANDROID_WRITE, whole);
            }catch(error){
                this.progress_close();
                console.error(error);
                alert(error);
            }
        },
```

以下が、スマホ側からの通知の受信に対応した処理です。

```js:js/start.js
        onDataChanged: async function(event){
            console.log('onDataChanged');

            let characteristic = event.target;
            console.log("characteristic: " + characteristic.uuid);

            try{
                var buffer = uint8array_to_array(characteristic.value);
                var operation = buffer[0];
                var offset = get_uint32b(buffer, 1);
                if( operation == OPERATION.READ ){
                    if(offset == 0){
                        read_data_length = get_uint32b(buffer, 1 + 4);
                    }
                    read_data_buffers.push(buffer.slice(1 + 4));
                    offset += buffer.length - (1 + 4);
                    if( offset < (4 + read_data_length + 2) ){
                        var whole = make_operation_array(OPERATION.READ, offset);
                        await this.writeChar(UUID_ANDROID_WRITE, whole);
                    }else{
                        var whole_length = read_data_buffers.reduce((sum, buffer) =>{
                            sum += buffer.length;
                            return sum;
                        }, 0);
                        var whole = new Uint8Array(whole_length);
                        read_data_buffers.reduce((index, buffer) =>{
                            whole.set(buffer, index);
                            return index += buffer.length;
                        }, 0);
                        var checksum = make_checksum(whole, 0, 4 + read_data_length);
                        if( checksum != get_uint16b(whole, 4 + read_data_length) ){
                            this.progress_close();
                            return;
                        }
                        this.read_raw = whole.slice(4, 4 + read_data_length);
                        this.read_received_date = new Date().getTime();
                        this.parse_read();
                        this.toast_show("データを取得しました。");
                        this.progress_close();
                    }
                }else
                if( operation == OPERATION.WRITE ){
                    console.log("OPERATION.WRITE offset=", offset);
                    var whole = new Uint8Array(1 + 4 + (write_data_array.length - offset));
                    whole[0] = OPERATION.WRITE;
                    set_uint32b(whole, offset, 1);
                    whole.set(write_data_array.slice(offset), 1 + 4);
                    if( (write_data_array.length - offset) > BLE_MTU_SIZE)
                        return this.writeChar(UUID_ANDROID_WRITE, whole.slice(0, BLE_MTU_SIZE));
                    else
                        return this.writeChar(UUID_ANDROID_WRITE, whole);
                }else
                if( operation == OPERATION.COMPLETE ){
                    console.log("OPERATION.COMPLETE offset=", offset);
                    this.progress_close();
                }else
                if( operation == OPERATION.ERROR ){
                    console.log("OPERATION.ERROR");
                    this.progress_close();
                }
            }catch(error){
                this.progress_close();
                console.error(error);
                alert(error);
            }
        },
```

# Cordovaアプリのコンパイル

> cd cordova\BleDataShareClient
> npm install
> cordova platform add android
> cordova build android
> cordova run android

# 動作画面

PC側

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/bcb354ca-3b19-641c-32ad-4972cc1332f0.png)

スマホ側

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/9ed7d0ed-7c23-6c63-cb67-ae9fe5cf2855.png)

# 参考

* [便利ページ：Web Bluetooth APIでBLEデバイスに接続](https://qiita.com/poruruba/items/653e9bbf6b725438442e)
* [Cordovaアプリ開発の備忘録](https://qiita.com/poruruba/items/f03b1691fd0f113735b3)

以上