キラッとやってみた!
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
// 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 の適当な箇所に以下を追加しましょう(^^
以下を最後あたりに追加してみましょうか。
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 接続用のボタンを配置しておきます。
では実装
//
// 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
// 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 機器として利用できるということです。