Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

キラッとやってみた!

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 は「親」になる機器と、その「親」に接続する「子」になる機器の2種類があります。「親」同士や「子」同士では接続できないので、どちらかを「親」に、もう片方を「子」にすることで接続します。
その時の「親」のことを「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

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 の適当な箇所に以下を追加しましょう(^^

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.plistRequired device capabilitiesbluetooth-leを追加します。

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

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

では実装

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
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 機器として利用できるということです。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away