前回の記事ではESP32とAE-TYBLE16を繋いで、Androidのデバッグアプリから接続を試してみました。
前回の記事を書いたあとに、こちらの有料Assetを使ったアプリで接続を試してみたんですが、どうしても接続できず、仕方なくUnity用のAndroidのプラグインを1から作成してみました。
Android側の処理は以下のサイトを参考にしています。
https://blog.fenrir-inc.com/jp/2013/10/bluetooth-le-android.html
開発環境
- Mac Book (10.13.6)
- Unity 2019.4.6f1
Androidプラグイン
Android Javaは普段書かないので書き方がおかしいかもしれません。
UnityプロジェクトのAssets/Plugins/Android以下追加します。
Android10以降だと位置情報の取得にユーザーの許可を取る必要があるため、今回はUnity上でターゲットAPIをAndroid 9以下にして回避します。
Android10以降では位置情報の許可を取らないとエラーが出ずにスキャン結果が返ってこないというところで躓きました。
Notificationのリクエストを送ることが重要みたいで、これがないと接続できませんでした。
package unity.android.plugin; import android.content.Context; import java.lang.Exception; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothManager; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCallback; import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothGattService; import android.bluetooth.le.*; import com.unity3d.player.UnityPlayer; import java.util.List; import java.util.ArrayList; import java.util.UUID; public class BluetoothLE { private static final String RECEIVE_OBJECT_NAME = "BluetoothLEReceiver"; private static final UUID CLIENT_CHARACTERISTIC_CONFIG = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); private BluetoothAdapter adapter; private BluetoothLeScanner scanner; private BluetoothGatt gatt; private BluetoothDevice device; // 初期化. public void initialize() { //Bluetoothアダプターを初期化 BluetoothManager manager = (BluetoothManager)UnityPlayer.currentActivity.getSystemService(Context.BLUETOOTH_SERVICE); adapter = manager.getAdapter(); scanner = adapter.getBluetoothLeScanner(); unitySendMessage("InitializeCallback"); } // スキャン開始. public void startScan() { ScanSettings.Builder scanSettings = new ScanSettings.Builder(); scanSettings.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY); ScanSettings settings = scanSettings.build(); // NOTE: Target Android9 API28まではマニフェスト追加のみで動作 Android10以降はユーザー許可が必要. scanner.startScan(null, settings, scanCallback); } // スキャンの停止. public void stopScan() { scanner.stopScan(scanCallback); } // デバイス接続. public void connectToDevice(String address) { device = adapter.getRemoteDevice(address); if (device == null) { return; } if (gatt != null) { gatt.disconnect(); } gatt = device.connectGatt(UnityPlayer.currentActivity, true, gattCallback); } // デバイス接続解除. public void disconnectDevice() { if (gatt != null) { gatt.disconnect(); gatt = null; } } private ScanCallback scanCallback = new ScanCallback() { @Override public void onScanResult(int callbackType, ScanResult result) { if(result.getDevice() == null) { return; } // 検出したデバイス情報を通知. String deviceName = result.getDevice().getName(); String address = result.getDevice().getAddress(); unitySendMessage("ScanCallback", deviceName, address); } }; private BluetoothGattCallback gattCallback = new BluetoothGattCallback() { @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int state) { if (state == BluetoothProfile.STATE_CONNECTED) { // 接続成功. unitySendMessage("ConnectCallback"); } else if (state == BluetoothProfile.STATE_DISCONNECTED) { // 接続解除. unitySendMessage("DisconnectCallback"); } } @Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { // 検出したサービスとCharacteristicを通知. for (BluetoothGattService service : gatt.getServices()) { for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) { unitySendMessage("DiscovereCharacteristicCallback", service.getUuid().toString(), characteristic.getUuid().toString()); } } } } }; // サービスを検出. public void discoverServices() { gatt.discoverServices(); } // Characteristicに対してNotificationの受信を要求. public void requestNotification(String serviceUUID, String notificationUUID) { BluetoothGattService service = gatt.getService(UUID.fromString(serviceUUID)); BluetoothGattCharacteristic characteristic = service.getCharacteristic(UUID.fromString(notificationUUID)); gatt.setCharacteristicNotification(characteristic, true); BluetoothGattDescriptor notification_descriptor = characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG); notification_descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); gatt.writeDescriptor(notification_descriptor); } // Unity側にメッセージ通知. private void unitySendMessage(String... params) { String param = String.join(",", params); UnityPlayer.UnitySendMessage(RECEIVE_OBJECT_NAME, "PluginMessage", param); } // メッセージ送信. public boolean sendMessage(String serviceUUID, String writeCharacteristicUUID, String message) { try { byte[] bytes = message.getBytes("UTF-8"); // 書き込み. BluetoothGattService service = gatt.getService(UUID.fromString(serviceUUID)); BluetoothGattCharacteristic characteristic = service.getCharacteristic(UUID.fromString(writeCharacteristicUUID)); characteristic.setValue(bytes); return gatt.writeCharacteristic(characteristic); } catch(Exception e) { unitySendMessage("ErrorCallback", e.getMessage()); } return false; } }
Android Manifest
こちらもUnityプロジェクトのAssets/Plugins/Android以下追加します。
当初Android10以降を対象にしていたためうまく動かず、その過程でパーミッションを色々追加したため少し過剰かもしれません。
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.unity3d.player" xmlns:tools="http://schemas.android.com/tools"> <uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> <uses-feature android:name="android.hardware.bluetooth_le" android:required="true" /> <application> <activity android:name="com.unity3d.player.UnityPlayerActivity" android:theme="@style/UnityThemeSelector"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <meta-data android:name="unityplayer.UnityActivity" android:value="true" /> </activity> </application> </manifest>
Unity
Androidプラグインを呼ぶ側
Unity側からAndroid側のメソッドを呼ぶための処理です。
using System; using UnityEngine; public static class BluetoothLE { private static AndroidJavaObject androidBle; private static BluetoothLEReceiver receiver; static BluetoothLE() { androidBle = new AndroidJavaObject("unity.android.plugin.BluetoothLE"); receiver = BluetoothLEReceiver.GetOrCreateReceiver(); } public static void Initialize(Action onInitialize, Action<string> onError) { receiver.OnInitialize = onInitialize; receiver.OnError = onError; androidBle.Call("initialize"); } public static void StartScan(Action<string, string> onScan) { receiver.OnScan = onScan; androidBle.Call("startScan"); } public static void StopScan() { androidBle.Call("stopScan"); } public static void Connect(string address, Action onConnect, Action onDisconnect) { receiver.OnConnect = onConnect; receiver.OnDisconnect = onDisconnect; androidBle.Call("connectToDevice", address); } public static void Disconnect() { androidBle.Call("disconnectDevice"); } public static void DiscoverService(Action<string, string> onDiscovereCharacteristic) { receiver.OnDiscovereCharacteristic = onDiscovereCharacteristic; androidBle.Call("discoverServices"); } public static bool SendMessage(string serviceUUID, string charcteristicUUID, string message) { return androidBle.Call<bool>("sendMessage", serviceUUID, charcteristicUUID, message); } public static void RequestNotification(string serviceUUID, string charcteristicUUID) { androidBle.Call("requestNotification", serviceUUID, charcteristicUUID); } }
Androidプラグインの受け側
AndroidからUnityへの通知はGameObject経由でないといけないため、インスタンス化したReceiverを用意します。
using System; using UnityEngine; public class BluetoothLEReceiver : MonoBehaviour { public static BluetoothLEReceiver instance; public Action OnInitialize; public Action<string> OnError; public Action<string, string> OnScan; public Action OnConnect; public Action OnDisconnect; public Action<string, string> OnDiscovereCharacteristic; public static BluetoothLEReceiver GetOrCreateReceiver() { if (instance == null) { instance = new GameObject("BluetoothLEReceiver").AddComponent<BluetoothLEReceiver>(); DontDestroyOnLoad(instance.gameObject); } return instance; } public void PluginMessage(string message) { Debug.Log(message); var param = message.Split(','); if (param.Length == 0) { return; } switch (param[0]) { case "InitializeCallback": OnInitialize?.Invoke(); break; case "ScanCallback": OnScan?.Invoke(param[1], param[2]); break; case "ConnectCallback": OnConnect?.Invoke(); break; case "DisconnectCallback": OnConnect?.Invoke(); break; case "DiscovereCharacteristicCallback": OnDiscovereCharacteristic?.Invoke(param[1], param[2]); break; case "ErrorCallback": OnError?.Invoke(param[1]); break; } } }
テストアプリ側
画面をタップすると値をAE-TYBLE16に送信するようにしています。
UUIDは記載したらマズそうなので伏せておきます。
ScanとDiscovereをしてみて、結果を出力すればきっとわかります。
using UnityEngine; using UnityEngine.UI; public class TYBLE16 : MonoBehaviour { public Text status; /* スキャン結果を見て追加 */ private const string DEVICE_NAME = "TYSA-B 3.0.0"; private const string DEVICE_ADDRESS = "**:**:**:**:**:**"; private const string SERVICE_UUID = "**************************************"; private const string NOTIFICATION_CHARACTERISTIC_UUID = "**************************************"; private const string WRITE_CHARACTERISTIC_UUID = "**************************************"; private bool connected; void Start() { BluetoothLE.Initialize(OnInitialize, OnError); } void OnInitialize() { BluetoothLE.StartScan(OnScan); status.text = "Scanning"; } void OnScan(string deviceName, string address) { if (deviceName == DEVICE_NAME && address == DEVICE_ADDRESS) { BluetoothLE.StopScan(); BluetoothLE.Connect(DEVICE_ADDRESS, OnConnect, OnDisconnect); status.text = "Connecting"; } } void OnConnect() { // 接続できたらサービスを検出. BluetoothLE.DiscoverService(OnDiscovereCharacteristic); } void OnDisconnect() { // 接続が切れたら再スキャン開始. BluetoothLE.StartScan(OnScan); status.text = "Scanning"; connected = false; } void OnDiscovereCharacteristic(string serviceUUID, string characteristicUUID) { if (serviceUUID == SERVICE_UUID && characteristicUUID == WRITE_CHARACTERISTIC_UUID) { BluetoothLE.RequestNotification(SERVICE_UUID, NOTIFICATION_CHARACTERISTIC_UUID); status.text = "RequestNotification"; connected = true; } } private void Update() { if (connected && Input.GetMouseButtonDown(0)) { // 画面をタップしたらメッセージ送信. BluetoothLE.SendMessage(SERVICE_UUID, WRITE_CHARACTERISTIC_UUID, "Hello World" + "\n"); } } void OnError(string message) { Debug.Log(message); } }
さいごに
これで無事に自前のAndroidアプリからAE-TYBLE16にBLE接続ができました。
しかしながら、AE-TYBLE16が受信したデータをマイコン側で受け取るまでに一定のラグがあるようです。
これはAE-TYBLE16の独自ファームウェアの機能によるものかと思いますが、
連続して値を送信すると最後に受信したタイミングから約1秒ほど待った後に蓄積したデータをまとめてマイコンに送っているような挙動になっています。
このままではスライダー操作で変化する値を送信するような使い方は難しそうなので、受信後に即時でマイコン側に値を渡せないか検証してみます。
2022/01/14 追記
受信が遅れる原因がわかりました。
Serial.readString()
がSerial.Timeout
で設定されている時間(Defaultが1000ms)まで待機するようです。
Serial.read()
で1文字ずつ処理をすれば即時で処理できることを確認できました。