THETA VよりBluetoothに対応した
RICOH THETA Vは今までのTHETAシリーズからBLEが対応し、よりアプリケーション開発の幅が拡がりました。また、Bluetooth Reference API & SDK | RICOH THETA DevelopersにてBluetooth API仕様が公開されているので今回はこちらを利用して撮影を行ってみます。
Web Bluetooth API
Bluetoothデバイスを気軽に試すプラットフォーム、特にデスクトップPCは意外に少なく、WindowsだとUWPに辛うじてWindows APIが存在するらしいということで結構発展途上な分野のようです。
今回はmacOSのChromeであれば、Web Bluetooth APIが標準で利用出来ると分かったのでサンプルHTMLを作ります。Web Bluetooth API に対応しているのはChrome(Google OS / macOS / Android / Linux)にて動作するようです(2018年1月現在)。各ブラウザの実装状況はweb-bluetooth/implementation-status.md at master · WebBluetoothCG/web-bluetoothで確認出来ます。
因みにAndroid Chromeも今回Nexus 5で試してみましたがペア設定まで出来ましたがその後接続が上手く行かなかったのでデバイスも選ぶ可能性があります。成功した方いましたら情報緩募です。
https必須
Web Bluetooth APIを動作させるにはhttps必須になっているようです。今回はGithub Pagesを使って公開すればhttpsが漏れなく付いてきて手軽に実現出来るのでスクリプト付きのindex.htmlを作成します。サンプルはWeb Bluetooth API Sample for THETA V、ソースコードはthetav-web-ble/index.htmlにて公開してあるので参考にしてください。
サンプルコード
素のJSコードだけでも書けたんですが、勉強も兼ねてVue.jsを使ってみます。BLEデバイスの接続状況とバッテリーレベルを参照しリアクティブに値を表示しています。
ビュー
<div id="app">
<p><label>Device Name: <input v-model="deviceName"></label></p>
<p><label>Auth UUID: <input v-model="authUUID"></label></p>
<p>
<button v-on:click="connect">Connect</button>
<button v-on:click="takePicture">Take Picture</button>
<button v-on:click="disconnect">Disconnect</button>
</p>
<p v-if="bluetooth.device">
Device Status: Name: {{bluetooth.device.name}},
Connected: {{bluetooth.device.gatt.connected}},
Battery Level: {{deviceBatteryLevel}}
</p>
<p>{{message}}</p>
</div>
スクリプト
const BLUETOOTH_CONTROL_SERVICE = '0f291746-0c80-4726-87a7-3c501fd3b4b6'
const AUTH_BLUETOOTH_DEVICE_CHARACTERISTIC = 'ebafb2f0-0e0f-40a2-a84f-e2f098dc13c3'
const SHOOTING_STATUS_SERVICE = '8af982b1-f1ff-4d49-83f0-a56db4c431a7'
const BATTERY_LEVEL_CHARACTERISTIC = '875fc41d-4980-434c-a653-fd4a4d4410c4'
const SHOOTING_CONTROL_SERVICE = '1d0f3602-8dfb-4340-9045-513040dad991'
const TAKE_PICTURE_CHARACTERISTIC = 'fec1805c-8905-4477-b862-ba5e447528a5'
let bluetooth = window.navigator.bluetooth
let app = new Vue({
el: '#app',
data: {
deviceName: '',
deviceBatteryLevel: 0,
authUUID: AUTH_BLUETOOTH_DEVICE_CHARACTERISTIC,
bluetooth: {},
message: ''
},
methods: {
connect: async () => {
let device = app.bluetooth.device = await bluetooth.requestDevice({
filters: [
{name: app.deviceName}
],
optionalServices: [
BLUETOOTH_CONTROL_SERVICE,
SHOOTING_STATUS_SERVICE,
SHOOTING_CONTROL_SERVICE
]
})
let server = app.bluetooth.server =
await device.gatt.connect()
let bluetooth_control_service =
await server.getPrimaryService(BLUETOOTH_CONTROL_SERVICE)
let auth_bluetooth_device_chacharacteristic =
await bluetooth_control_service.getCharacteristic(AUTH_BLUETOOTH_DEVICE_CHARACTERISTIC)
await auth_bluetooth_device_chacharacteristic.writeValue(new TextEncoder().encode(app.authUUID))
app.message = `Device "${app.deviceName}" is connected.`
let shooting_status_service =
await server.getPrimaryService(SHOOTING_STATUS_SERVICE)
let battery_level_characteristic = app.bluetooth.device.battery_level_characteristic =
await shooting_status_service.getCharacteristic(BATTERY_LEVEL_CHARACTERISTIC)
let battery_level = await battery_level_characteristic.readValue()
app.deviceBatteryLevel = battery_level.getUint8(0)
},
disconnect: async () => {
await app.bluetooth.device.gatt.disconnect()
app.message = `Device "${app.deviceName}" is disconnected.`
},
takePicture: async () => {
let server = app.bluetooth.server
let shooting_control_service =
await server.getPrimaryService(SHOOTING_CONTROL_SERVICE)
let take_picture_characteristic =
await shooting_control_service.getCharacteristic(TAKE_PICTURE_CHARACTERISTIC)
await take_picture_characteristic.writeValue(new Uint8Array([1]))
app.message = "Take Picture is succeed."
}
}
})
撮影までのWeb Bluetooth APIを読む
それでは撮影までのWeb Bluetooth API部分の実装について説明していきます。
window.navigator.bluetooth
let bluetooth = window.navigator.bluetooth
ブラウザがWeb Bluetooth APIに対応している場合はwindow.navigator.bluetoothが存在します。
requestDevice
let device = app.bluetooth.device = await bluetooth.requestDevice({
filters: [
{name: app.deviceName}
],
optionalServices: [
BLUETOOTH_CONTROL_SERVICE,
SHOOTING_STATUS_SERVICE,
SHOOTING_CONTROL_SERVICE
]
})
requestDeviceはデバイスアクセス許可をユーザーに求めます。filtersによって検知されたデバイスのペア設定ダイアログが出ます。THETA Vではnameはシリアル番号の英字を取り除いた数字8桁部分で指定します。
optionalServicesはアクセス許可を求めるサービスを指定します。これが無いとCharacteristicの読み書きが出来ないので、使用するCharacteristicが属するServiceを事前に指定しておく必要があります。
connect
let server = app.bluetooth.server =
await device.gatt.connect()
Bluetoothデバイスに接続します。GATTとは、BLEの通信規定のことでServiceの中のCharacteristicを読み書きしてデバイス操作をします。Service、Characteristicの識別子はどちらもUUIDによって定義されます。
getPrimaryService
let shooting_control_service =
await server.getPrimaryService(SHOOTING_CONTROL_SERVICE)
サービスを参照します。また、全てのサービスを取得する場合はgetPrimaryServices()を呼び出します。
getCharacteristic
let take_picture_characteristic =
await shooting_control_service.getCharacteristic(TAKE_PICTURE_CHARACTERISTIC)
キャラクタリスティックを参照します。また、サービスの全てのキャラクタリスティックを参照する場合はgetCharacteristics()を呼び出します。
readValue / writeValue
let battery_level = await battery_level_characteristic.readValue()
app.deviceBatteryLevel = battery_level.getUint8(0)
await take_picture_characteristic.writeValue(new Uint8Array([1]))
キャラクタリスティックを読み書きします。値はバイト列で扱います。文字列はTHETA Vの場合、utf8sでバイト列に変換します。デバイス名を書き込む時の例:
await chacharacteristic.writeValue(new TextEncoder().encode(string))
THETA VをWeb Bluetooth API叩いてみて感想
BLEの通信プロトコル自体はキャラクタリスティックにバイナリ列を読み書きしろという単純な仕組みになっているので実際やるまでは実装大変なんじゃと思ってましたが要領得てしまえば比較的スムーズに撮影するところまで辿り付くことが出来ました。THETA VにはUSBのMTP、Wi-FiのOSCv2、BLEのGATTの3つのAPIが提供されましたが、とっつきやすさはMTPとOSCの間くらいな位置づけになっていると思います。
また、THETA VのBluetooth APIで残念な点は、撮影を行うには電源がONになっている必要がありますが、THETA Vにはスリープが存在しています。当然その状態では撮影は出来ないのですがBLEは接続できていてここからカメラ本体を起こすAPI仕様がまだ公開されていないようです。公式スマホアプリでは起こしているようなのでAPIは存在してそうで、折角のBLEの魅力が半減してしまっているので一刻も早い公開を期待します。