RaspberryPi3 を Bluetooth(Peripheral) にして iPhone と通信してみた

キラッとやってみた！

## Bluetoothについて
いろいろ規格があるようです。
　→　https://time-space.kddi.com/special/it_words/20140515/
　→　https://ja.wikipedia.org/wiki/Bluetooth

RaspberryPi3 は Bluetooth4.1 / BluetoothLE が使用できるようです。
iPhone の場合も概ね 4.1 以上なので、接続できそうですね。

### Central（セントラル） と Peripheral（ペリフェラル）
「中央」と「周辺」です。
Bluetooth は「親」になる機器と、その「親」に接続する「子」になる機器の２種類があります。「親」同士や「子」同士では接続できないので、どちらかを「親」に、もう片方を「子」にすることで接続します。
その時の「親」のことを「Central（セントラル）」「子」のことを「Peripherap（ペリフェラル）」と呼んでいます。
例えば、PCに無線マウスを接続する場合、PCが「親」で、無線マウスが「子」になります。

今回は、RaspberryPi側 で取得した情報を iPhone に投げて、状況を見たり処理をしたりする、ということを想定して、Central を iPhone に、Peripherarl を RaspberryPi にしてみました。


## RaspberryPi3 で Bluetooth Peripheral
ネットで探してもなかなか出てこなかったのですが、Node.js の bleno というモジュールを使うとできるっぽかったので、やってみました。

### bleno インストール

nodebrew をインストールして、npm で bleno をインストールする方法にします。

　→ https://github.com/noble/bleno
　→ http://dream-of-electric-cat.hatenablog.com/entry/2015/04/13/221940

まずは各種ツールのインストール

```
# apt-get update
# apt-get install bluetooth bluez libbluetooth-dev libudev-dev
# apt-get install libdbus-1-dev libdbug-glib-1-dev libglib2.0-dev libical-dev
```

nodebrew をインストール

```
$ curl -L git.io/nodebrew | perl - setup
```

完了したらパスを通しておきます。

```
$ vi ~/.bashrc
export PATH="$HOME/.nodebrew/current/bin:$PATH"

んで
$ source ~/.bashrc
とか
```

### Node.js のバージョン
ここがとても悩みました。
とりあえず最新版（11.2.0）を入れて ```$ npm install bleno```　としてもエラーでインストールできないのです。
ネット上にも情報はなく、、、ただ、version 8.x とかでうまくいっている例はあったので、とりあえず、バージョンを下げてインストールするという方法で(^^;

```
$ nodebrew install-binary 9.11.2
$ nodebrew use 9.11.2

$ cd apppath
$ npm install bleno
```

とすると warning は出ますがインストールできました！


## キャラクタリスティックとかアドバタイズとか
日本語でおk　状態です(^^;
よくわかりませんが、通信の本体と（ペリフェラルからの）広告・告知、的なイメージです。

上の方で、「親」と「子」と書きましたが、「子」である peripheral は、「自分はこれこれこういうサービスをしている端末だよー」と広告・告知（アドバタイズ）を行うことで「親」に見つけてもらい、互いに接続する、という仕組みになっています。なので「子」は初めに自分のサービスについて定義し告知する必要があります。そして、うまく見つけてもらい接続ができたら、キャラクタリスティック（通信の本体）を通してデータのやり取りを行います。

### サービス定義
サービスには、その名前と、特定するための UUID を定義する必要があります。

UUID は何かルールがあるようですが、今回は 'FF00' にしました。
 → http://jellyware.jp/kurage/bluejelly/uuid.html
＃ちゃんとする場合は長いUUIDか定義されているものを使いましょう。

### キャラクタリスティック定義
次に通信の本体であるキャラクタリスティックを定義します。
特定するための UUID（サービスと被らないように）と、属性（property）とその処理を定義します。
property はテストなので、read、write、notify を使用します。
read は 「親」からの読み込み、write は 「親」からの書き込み、notify は「子」からの通知、です。

次項実装中の onReadRequest とか onWriteRequest が各プロパティの処理部分になります。
notify の場合は、notify要求を受け付けた後に、任意のタイミングで「親」へデータを送信する処理を記述します。→ onSubscribe -> sendNotification

## では実装
参考
　→ https://qiita.com/uzuki_aoba/items/346e28b6e9170ce85a6c
　→ https://qiita.com/kentarohorie/items/b9549af9c71886860866

```javascript:test.js
// bluetooth peripheral test
require("date-utils");
const bleno = require("bleno");
const util = require("util");

// configuration bleno
const MyName = "rasp-blue-test";
const MyServiceUUID = "FF00";
const serviceUUIDs = [MyServiceUUID];
const CommCharacteristicUUID = "FF01";

// create characteristic
const CommCharacteristic = function() {
  CommCharacteristic.super_.call(this, {
    uuid: CommCharacteristicUUID,
    properties: ["read", "write", "notify"]
  });
  this._updateValueCallback = null;
};
util.inherits(CommCharacteristic, bleno.Characteristic);

CommCharacteristic.prototype.onReadRequest = function(offset, callback){
  var dt = new Date();
  var str = dt.toFormat("YYYY-MM-DD HH24:MI:SS");
  console.log("read request -> " + str);
  var data = Buffer.from(str, "UTF-8"); 
  callback(this.RESULT_SUCCESS, data);
};
CommCharacteristic.prototype.onWriteRequest = function(data, offset, withoutResponse, callback){
  console.log("write request: " + data.toString("UTF-8"));
  callback(this.RESULT_SUCCESS);
};

CommCharacteristic.prototype.onSubscribe = function(maxValueSize, updateValueCallback){
  console.log("registed notification.");
  this._updateValueCallback = updateValueCallback;
};
CommCharacteristic.prototype.onUnsbscribe = function(){
  console.log("un-registed notification.");
  this._updateValueCallback = null;
};

CommCharacteristic.prototype.sendNotification = function(val){
  if(this._updateValueCallback != null){
    this._updateValueCallback(val);
    console.log("send notification: " + val);
  } else {
    console.log("can not send notification!");
  }
};
const commChara = new CommCharacteristic();

// create Service
const MyService = new bleno.PrimaryService({
  uuid: MyServiceUUID,
  characteristics: [ commChara ]
});

// --------------------------------------------------------------------------- 
// bluetooth event
bleno.on("stateChange", function(state) {
  console.log("stateChange: " + state);
  if(state == "poweredOn"){
    bleno.startAdvertising(MyName, serviceUUIDs, function(error){
      if(error) console.error(error);
    });
  } else {
    bleno.stopAdvertising();
  }
});

bleno.on("advertisingStart", function(error){
  if(!error){
    console.log("start advertising...");
    bleno.setServices([MyService]);
  } else {
    console.error(error);
  }
});

// ---------------------------------------------------------------------------
function exit(){
  process.exit();
}
process.on('SIGINT', exit);
```

では、実行してみましょう。
実行するためには、管理者権限が必要になります。

```
# node test.js
```

正しく動いているかの確認には、```LightBlue```　というアプリがあるので、それを iPhone にインストールして確認します。

定義した名前「rasp-blue-test」が表示されていれば、告知成功です。
さらに中に入って、様子を見る（read、write のテスト）こともできます。

## Notification やってみた
上記のままだと notification のテストができないので、外部から通知ができるように、express(HTTPサーバ）を追加します。

```
$ npm install express
```

先ほどの test.js の適当な箇所に以下を追加しましょう(^^

```javascript:test.js

以下を最後あたりに追加してみましょうか。

const express = require("express");

// configuration express
const httpport = 8012;
const httpsrv = express();
httpsrv.use(express.static("./"));

// ---------------------------------------------------------------------------
// express(httpsrv)
httpsrv.get("/", (req, res) => {
  res.send("Hello, world!");
});
httpsrv.get("/notify", (req, res) => {
  var val = req.query.v;
  var data = Buffer.from(val, "UTF-8");
  commChara.sendNotification(data);
  res.send("");
});
httpsrv.listen(httpport);
```

そして実行します。

```
# node test.js
```

ブラウザでもいいですので、```http://localhost:8012/notify?v=Hello```
とすると、通知が飛ぶようになります。
先ほどのアプリで確認してみましょう。


## CoreBluetooth で Bluetooth Central
では iPhone 側を作成します。
　→ https://qiita.com/shu223/items/78614325ce25bf7f4379

### 各種設定
コーディングする前に各種設定があるので行います。

```Info.plist``` の ```Required device capabilities``` に ```bluetooth-le```を追加します。

次に ```Background Modes``` を ON にして、```Uses Bluetooth LE accessories``` にチェックを入れます。

次に xibファイルに状態表示用のラベルと、Read、Write用のボタンと、bluetooth 接続用のボタンを配置しておきます。
 
### では実装

```objective_c:ViewController.h
//
//  ViewController.h
//  BluetoothTest
//
//  Created by SOMEI Yoshino on 2018/11/17.
//  Copyright © 2018 sphear. All rights reserved.
//

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController

@property(nonatomic, strong) IBOutlet UIButton *btscanBtn; // bluetooth接続用ボタン
@property(nonatomic, strong) IBOutlet UILabel *statLabel; // 状態表示ラベル
@property(nonatomic, strong) IBOutlet UIButton *readBtn; // readボタン
@property(nonatomic, strong) IBOutlet UIButton *writeBtn; // writeボタン

@end
```

```objective_c:ViewController.m
//
//  ViewController.m
//  BluetoothTest
//
//  Created by SOMEI Yoshino on 2018/11/17.
//  Copyright © 2018 sphear. All rights reserved.
//

#import "ViewController.h"
#import "CoreBluetooth/CoreBluetooth.h"

#define TRANSFER_SERVICE_UUID           @"FF00" // Node.js の時につけたサービスUUID
#define TRANSFER_CHARACTERISTIC_UUID    @"FF01" // 同じくキャラクタリスティックのUUID

@interface ViewController () <CBCentralManagerDelegate, CBPeripheralDelegate>
{
    CBCentralManager *cbManager;
    CBPeripheral *btPeripheral;
    CBCharacteristic *btCharacteristic;
    
    BOOL isConnected;
}
@end

@implementation ViewController

@synthesize btscanBtn;
@synthesize statLabel;
@synthesize readBtn;
@synthesize writeBtn;

// ---------------------------------------------------------------------------
- (IBAction)pushReadBluetoothButton:(id)sender {
    if(btCharacteristic == nil) return;
    [btPeripheral readValueForCharacteristic:btCharacteristic];
}
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
    if(error){
        NSLog(@"read error: %@", error);
        return ;
    }
    NSString *str = [[NSString alloc] initWithData:characteristic.value encoding:NSUTF8StringEncoding];
    NSLog(@"read: UUID %@, %@", characteristic.UUID, str);
    statLabel.text = [NSString stringWithFormat:@"%@", str];
}

// ---------------------------------------------------------------------------
- (IBAction)pushWriteBluetoothButton:(id)sender {
    if(btCharacteristic == nil) return;
    NSData *data = [@"Hello, world!" dataUsingEncoding:NSUTF8StringEncoding];
    //unsigned char val = 0x01;
    //NSData *data = [[NSData alloc] initWithBytes:&val length:1];
    [btPeripheral writeValue:data forCharacteristic:btCharacteristic type:CBCharacteristicWriteWithResponse];
}
- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
    if(error){
        NSLog(@"write error: %@", error);
        return;
    }
    NSLog(@"write success!");
    statLabel.text = @"write success!";
}

// ---------------------------------------------------------------------------
- (IBAction)pushBluetoothScanButton:(id)sender {
    if(btPeripheral != nil){
        [self cleanupBluetooth];
        return;
    }
    isConnected = NO;
    btscanBtn.titleLabel.text = @"Bluetooth disconnect";
    btscanBtn.enabled = NO;

    cbManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil];
}
- (void)centralManagerDidUpdateState:(CBCentralManager *)central {
    switch(central.state){
        case CBManagerStatePoweredOn:
            NSLog(@"power on! (%ld)", (long)central.state);
            statLabel.text = @"power on";
            // start scan!
            [self startScanBluetooth];
            return;
            break;
        case CBManagerStatePoweredOff:
            NSLog(@"power off (%ld)", (long)central.state);
            statLabel.text = @"power off";
            break;
        case CBManagerStateUnsupported:
            NSLog(@"unsupported... (%ld)", (long)central.state);
                statLabel.text = @"unsupported...";
            break;
        case CBManagerStateUnauthorized:
            NSLog(@"unauthenticated... (%ld)", (long)central.state);
            statLabel.text = @"unauthenticated...";
            break;
        case CBManagerStateResetting:
            NSLog(@"restarting... (%ld)", (long)central.state);
            statLabel.text = @"restarting...";
            break;
        case CBManagerStateUnknown:
            NSLog(@"unknown state... (%ld)", (long)central.state);
            statLabel.text = @"unknown state";
            break;
        default:
            NSLog(@"... (%ld)", (long)central.state);
    }
    btscanBtn.enabled = YES;
}

// ---- scaning bluetooth peripherals ----
- (void)startScanBluetooth {
    NSDictionary *opt = [NSDictionary dictionaryWithObjects:@[[NSNumber numberWithBool:NO]] forKeys:@[CBCentralManagerScanOptionAllowDuplicatesKey]];
    
    //[cbManager scanForPeripheralsWithServices:nil options:opt];
    CBUUID *serviceuuid = [CBUUID UUIDWithString:TRANSFER_SERVICE_UUID];
    [cbManager scanForPeripheralsWithServices:@[serviceuuid] options:opt];
    
    NSLog(@"scaning bluetooth peripheral...");
    statLabel.text = @"scaning bluetooth peripheral...";
}
- (void)stopScanBluetooth {
    [cbManager stopScan];
    NSLog(@"stoped scaning bluetooth.");
}

// ---- discovered peripheral ----
-(void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *,id> *)advertisementData RSSI:(NSNumber *)RSSI {
    if(btPeripheral != nil) {
        NSLog(@"Already discovered peripherals.");
        return;
    }

    NSLog(@"discovered %@ at %@", peripheral.name, RSSI);
    statLabel.text = @"discovered peripheral";
    [self stopScanBluetooth];

    NSLog(@"connecting...");
    btPeripheral = peripheral;
    [central connectPeripheral:btPeripheral options:nil];
}

- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error {
    NSLog(@"connection failed..");
    [self cleanupBluetooth];
}
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral {
    NSLog(@"connected! discovering service...");
    peripheral.delegate = self;
    //[peripheral discoverServices:nil];
    CBUUID *serviceuuid = [CBUUID UUIDWithString:TRANSFER_SERVICE_UUID];
    [peripheral discoverServices:@[serviceuuid]];
}

// ---- discovered (bluetooth peripheral) services ----
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error {
    if (error) {
        [self cleanupBluetooth];
        return;
    }
    
    NSLog(@"discovered services");
    for(CBService *srv in peripheral.services){
        NSLog(@"  %@: %@", srv.UUID, srv.description);
    }
    
    CBUUID *charactaristicuuid = [CBUUID UUIDWithString:TRANSFER_CHARACTERISTIC_UUID];
    for(CBService *srv in peripheral.services) {
        [peripheral discoverCharacteristics:@[charactaristicuuid] forService:srv];
    }
}

// ---- discovered (bluetooth peripheral service) characteristics ----
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error {
    if(error){
        [self cleanupBluetooth];
        return;
    }
    isConnected = YES;
    btscanBtn.enabled = YES;
    
    NSArray *charstics = service.characteristics;
    NSLog(@"find %d characteristics", charstics.count);
    NSLog(@"%@", charstics);
    
    btCharacteristic = [charstics objectAtIndex:0];
    
    // accept notification from bluetooth peripheral
    //   if notify stop, set NO!
    [btPeripheral setNotifyValue:YES forCharacteristic:btCharacteristic];
}

- (void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
    if(error){
        NSLog(@"notify state error: %@", error);
    } else {
        NSLog(@"notify set success: %d", characteristic.isNotifying);
        if(characteristic.isNotifying == 0){
            [cbManager cancelPeripheralConnection:btPeripheral];
        }
    }
}

// ---------------------------------------------------------------------------
- (void)cleanupBluetooth {
    // See if we are subscribed to a characteristic on the peripheral
    if (btPeripheral.services != nil) {
        for (CBService *service in btPeripheral.services) {
            if (service.characteristics != nil) {
                for (CBCharacteristic *characteristic in service.characteristics) {
                    if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:TRANSFER_CHARACTERISTIC_UUID]]) {
                        if (characteristic.isNotifying) {
                            [btPeripheral setNotifyValue:NO forCharacteristic:characteristic];
                            return;
                        }
                    }
                }
            }
        }
    }
    [cbManager cancelPeripheralConnection:btPeripheral];
}
- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error {
    if(error){
        NSLog(@"disconnect error: %@", error);
    } else {
        NSLog(@"peripheral disconnected!");
        statLabel.text = @"bluetooth disconnected";
        btCharacteristic = nil;
        btPeripheral = nil;
        cbManager = nil;
        isConnected = NO;
        btscanBtn.titleLabel.text = @"Bluetooth Scan";
        btscanBtn.enabled = YES;
    }
}

// ---------------------------------------------------------------------------
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    cbManager = nil;
    btPeripheral = nil;
    btCharacteristic = nil;
    isConnected = NO;
}

@end
```

流れとしては、デバイスの電源が ON になったら、bluetooth スキャンを開始して peripheral を検索し、指定した サービスUUID があれば接続して、キャラクタリスティックを取得して、データのやり取りを行う、というイメージです。

## まとめ
bluetooth 面白いですね。
ハードル高そうでしたが、わかってしまえば簡単に使えるっぽいです。

今回はセンサー等使いませんでしたが、折角の RaspberryPi ですので、スイッチからの入力とかLEDチカチカとかさせるともっと面白いかもです。

それから、notification通知 の起点に express(HTTP) を使いましたが、便利ですねー。これで Node.js 以外の言語からも RaspberryPi Peripheral 機器として利用できるということです。