2016年8月23日 星期二

[Unity][Android] 自製 Unity Bluetooth Low Energy 4.0 Plugin 紀錄

程式碼參閱 Google android-BluetoothLeGatt-master 範例改寫,產生 classes.jar 檔供使用在 Unity 5.x Personal 版的 Android 平台上,使其可進行檢查藍芽功能、開啟藍芽功能、搜尋 BLE、連接 BLE 等藍芽操作功能。

透過引用 Unity Mono classes.jar 繼承 UnityPlayerActivity 的 Activity 中加入一個 callback function interface,以便 BroadcastReceiver  收到藍芽斷線、連線、服務、資料等訊息時,主動回傳到 Unity 中處理。



關於 Android Library 專案的 classes.jar 檔取得方式參考另一篇:
Android Studio 建立與使用 AAR Library

Unity 5.x Personal 之 Mono 的 Classes.jar 檔案位於(依 Unity 版本不同,檔案的路徑會有所不同):
C:\Program Files\Unity\Editor\Data\PlaybackEngines\AndroidPlayer\Variations\mono\Release\Classes\classes.jar
__________________________________________________

檔案:BleServiceOnReceiveCallback.java

package com.company.unityandroidbleplugin;

public interface BleServiceOnReceiveCallback {
    void BleServiceOnConnect(String action);
    void BleServiceOnDisconnect(String action);
    void BleServiceOnDiscovered(String action);
    void BleServiceOnDataAvailable(String WriteData, String ReadData);
}
__________________________________________________

檔案:BluetoothLeService.java
略,參考 Google android-BluetoothLeGatt-master 範例。
__________________________________________________

檔案:SampleGattAttributes.java
略,參考 Google android-BluetoothLeGatt-master 範例。
__________________________________________________

檔案:MainActivity.java

package com.company.unityandroidbleplugin;

import android.app.Activity;
import android.app.AlertDialog;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Toast;

import com.unity3d.player.UnityPlayer;
import com.unity3d.player.UnityPlayerActivity;

import java.util.ArrayList;

public class MainActivity extends UnityPlayerActivity {
    private final static String TAG = MainActivity.class.getSimpleName();
    public static String UnityObjectName = "Main Camera";
    private static final int REQUEST_ENABLE_BT = 1;

    private Activity _activity;

    public boolean mScanning;
    private boolean mConnected = false;
    private String mDeviceAddress;
    private BluetoothLeService mBluetoothLeService;
    private LeDeviceListAdapter mLeDeviceListAdapter;
    private BluetoothAdapter mBluetoothAdapter;
    private BleServiceOnReceiveCallback BleServiceReceiveCB = null;

    // 搜尋藍芽的 Callback function。
    private BluetoothAdapter.LeScanCallback mLeScanCallback =
            new BluetoothAdapter.LeScanCallback() {
                @Override
                public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {
                    // 這目前還是引用內定的 BaseAdapter 存放裝置清單,也可捨去 BaseAdapter 然後自行使用一個 ArrayList 去存取
                    mLeDeviceListAdapter.addDevice(device);
                    mLeDeviceListAdapter.notifyDataSetChanged();
                }
            };

    private final ServiceConnection mServiceConnection = new ServiceConnection() {

        @Override
        public void onServiceConnected(ComponentName componentName, IBinder service) {
            mBluetoothLeService = ((BluetoothLeService.LocalBinder) service).getService();
            if (!mBluetoothLeService.initialize()) {
                UnityLog(TAG, "Unable to initialize Bluetooth");
                finish();
            }
            // Automatically connects to the device upon successful start-up initialization.
            mBluetoothLeService.connect(mDeviceAddress);
        }

        @Override
        public void onServiceDisconnected(ComponentName componentName) {
            mBluetoothLeService = null;
        }
    };

    private final BroadcastReceiver mGattUpdateReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            final String action = intent.getAction();

            if (BluetoothLeService.ACTION_GATT_CONNECTED.equals(action)) {
                mConnected = true; //connected
                BleServiceReceiveCB.BleServiceOnConnect(action);
            } else if (BluetoothLeService.ACTION_GATT_DISCONNECTED.equals(action)) {
                mConnected = false; //disconnected
                BleServiceReceiveCB.BleServiceOnDisconnect(action);
            } else if (BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED.equals(action)) {
                BleServiceReceiveCB.BleServiceOnDiscovered(action);
                // 顯示 GATT 詳細 Service 和特徵值請參考 Google 的範例程式。
                // Show all the supported services and characteristics on the user interface.
                //displayGattServices(mBluetoothLeService.getSupportedGattServices());
            } else if (BluetoothLeService.ACTION_DATA_AVAILABLE.equals(action)) {
                // 這幾個資料依照自己 BluetoothLeService 提供的資料做取捨。
                String r = intent.getStringExtra(BluetoothLeService.READ_DATA);
                String w = intent.getStringExtra(BluetoothLeService.WRITE_DATA);
                BleServiceReceiveCB.BleServiceOnDataAvailable(r, w);
            }
        }
    };

    public void SetBleServiceOnReceiveCallback(BleServiceOnReceiveCallback cb) {
        BleServiceReceiveCB = cb;
    }

    public void RemoveBleServiceOnReceiveCallback() {
        BleServiceReceiveCB = null;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        _activity = UnityPlayer.currentActivity;
    }

    private static IntentFilter makeGattUpdateIntentFilter() {
        final IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(BluetoothLeService.ACTION_GATT_CONNECTED);
        intentFilter.addAction(BluetoothLeService.ACTION_GATT_DISCONNECTED);
        intentFilter.addAction(BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED);
        intentFilter.addAction(BluetoothLeService.ACTION_DATA_AVAILABLE);
        return intentFilter;
    }

    // 供除錯用,可省略。
    public static void UnityLog(String tag, String message){
        UnityPlayer.UnitySendMessage(UnityObjectName, "AndroidPluginMessage", tag + " : " + message);
    }

    // 顯示 Toast。
    public void ShowToast(final String message, final int length){
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(_activity, message, length).show();
            }
        });
    }

    // 顯示 Alert。
    public void ShowAlert(final String title, final String message){
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                AlertDialog.Builder bld = new AlertDialog.Builder(_activity);
                bld.setTitle(title).setMessage(message).setNeutralButton("OK", new OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        // 加入按下 OK 後想做的事。
                    }
                });
                bld.create().show();
            }
        });
    }

    // 藍芽功能檢查、初始化、提示開啟藍芽。
    public boolean InitializeBluetooth() {
        // Check API Level. For API level 18 and above.
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
            UnityLog(TAG, "Only support Android version is 4.3 or above.");
            return false;
        }

        // Use this check to determine whether BLE is supported on the device.  Then you can
        // selectively disable BLE-related features.
        if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
            UnityLog(TAG, "BLE not supported");
            return false;
        }

        // Initializes a Bluetooth adapter.  For API level 18 and above, get a reference to
        // BluetoothAdapter through BluetoothManager.
        final BluetoothManager bluetoothManager =
                (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
        mBluetoothAdapter = bluetoothManager.getAdapter();

        // Checks if Bluetooth is supported on the device.
        if (mBluetoothAdapter == null) {
            UnityLog(TAG, "error bluetooth not supported");
            return false;
        }

        // Ensures Bluetooth is enabled on the device.  If Bluetooth is not currently enabled,
        // fire an intent to display a dialog asking the user to grant permission to enable it.
        if (!mBluetoothAdapter.isEnabled()) {
            if (!mBluetoothAdapter.isEnabled()) { //check twice
                Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
                startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
            }
        }

        // Initializes list view adapter.
        mLeDeviceListAdapter = new LeDeviceListAdapter();

        registerReceiver(mGattUpdateReceiver, makeGattUpdateIntentFilter());

        Intent gattServiceIntent = new Intent(this, BluetoothLeService.class);
        this.getApplicationContext().bindService(gattServiceIntent, mServiceConnection, BIND_AUTO_CREATE);

        return true;
    }

    // 連接 BLE 裝置。
    public boolean ConnectBleDevice(String mDeviceAddress) {
        this.mDeviceAddress = mDeviceAddress;
        return mBluetoothLeService.connect(mDeviceAddress);
    }

    // 斷開與 BLE 裝置的連線。
    public void DisconnectBleDevice(){
        mBluetoothLeService.disconnect();
    }

    // 掃描或停止掃描附近的 BLE 裝置。
    public void scanLeDevice(final boolean enable) {
        if (enable) {
            mScanning = true;
            mBluetoothAdapter.startLeScan(mLeScanCallback);
        } else {
            mScanning = false;
            mBluetoothAdapter.stopLeScan(mLeScanCallback);
        }
    }

    // 取得已被發現的 BLE 裝置總數。
    public int GetBleDeviceCount() {
        return mLeDeviceListAdapter.getCount();
    }

    // 取得 BLE 裝置。
    public BluetoothDevice GetBleDevice(int position) {
        return mLeDeviceListAdapter.getDevice(position);
    }

    // 取得 BLE 裝置名稱。
    public String GetBleDeviceName(int position) {
        return mLeDeviceListAdapter.getDevice(position).getName();
    }

    // 取得 BLE 裝置的 IP 位址。
    public String GetBleDeviceAddress(int position) {
        return mLeDeviceListAdapter.getDevice(position).getAddress();
    }

    // Adapter for holding devices found through scanning.
    private class LeDeviceListAdapter extends BaseAdapter {
        private ArrayList mLeDevices;

        public LeDeviceListAdapter() {
            super();
            mLeDevices = new ArrayList();
        }

        public void addDevice(BluetoothDevice device) {
            if (device.getName() != null) {
                if (!mLeDevices.contains(device)) {
                    mLeDevices.add(device); //如果 BLE 裝置未在清單內就加入清單。
                    // 這邊也可以增加一個 callback function,將新增的 device name 和 address 傳到 Unity 使用。
                }
            }
        }

        public BluetoothDevice getDevice(int position) {
            return mLeDevices.get(position);
        }

        public void clear() {
            mLeDevices.clear();
        }

        @Override
        public int getCount() {
            return mLeDevices.size();
        }

        @Override
        public Object getItem(int i) {
            return mLeDevices.get(i);
        }

        @Override
        public long getItemId(int i) {
            return i;
        }

        @Override
        public View getView(int i, View view, ViewGroup viewGroup) {
            return view;
        }
    }
}
__________________________________________________

檔案:AndroidPlugin.cs

using UnityEngine;
using System.Collections;

public class AndroidPlugin : MonoBehaviour
{
    private static AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
    private static AndroidJavaObject jo = jc.GetStatic("currentActivity");

    public static void ShowToast(string message, int length)
    {
        jo.Call("ShowToast", message, length);
    }

    public static void ShowAlert(string title, string message)
    {
        jo.Call("ShowAlert", title, message);
    }

    public static bool InitializeBluetooth()
    {
        return jo.Call<bool>("InitializeBluetooth");
    }

    public static void scanLeDevice(bool enable)
    {
        jo.Call("scanLeDevice", enable);
    }
 
    public static bool ConnectBleDevice(string mDeviceAddress)
    {
        return jo.Call<bool>("ConnectBleDevice", mDeviceAddress);
    }

    public static void DisconnectBleDevice()
    {
        jo.Call("DisconnectBleDevice");
    }

    public static void SetBleServiceOnReceiveCallback(AndroidJavaProxy bleServiceOnReceiveCallback)
    {
        jo.Call("SetBleServiceOnReceiveCallback", bleServiceOnReceiveCallback);
    }

    public static void RemoveBleServiceOnReceiveCallback()
    {
        jo.Call("RemoveBleServiceOnReceiveCallback");
    }

    public static int GetBleDeviceCount()
    {
        return jo.Call<int>("GetBleDeviceCount");
    }

    public static int GetBleDeviceCount()
    {
        return jo.Call<int>("GetBleDeviceCount");
    }

    public static string GetBleDeviceName(int position) {
        return jo.Call<string>("GetBleDeviceName", position);
    }

    public static string GetBleDeviceAddress(int position) {
        return jo.Call<string>("GetBleDeviceAddress", position);
    }
}
__________________________________________________

檔案:TestScript.cs
public class TestScript : MonoBehaviour
{
    ......
    void Start()
    {
        ......
        // 藍芽初始化。
        AndroidPlugin.InitializeBluetooth();

        // 掃描 BLE 裝置。
        AndroidPlugin.scanLeDevice(true);

       // 停止掃描 BLE 裝置。
        AndroidPlugin.scanLeDevice(false);

        // 設定 BleService 的 Callback function。
        BleServiceOnReceiveCallback bleServiceOnReceiveCallback = new bleServiceOnReceiveCallback();
        AndroidPlugin.SetBleServiceOnReceiveCallback(bleServiceOnReceiveCallback);
        ......
    }
    ......
    public class BleServiceOnReceiveCallback : AndroidJavaProxy
    {
        public BleServiceOnReceiveCallback():base("com.company.unityandroidbleplugin.BleServiceOnReceiveCallback")
        {
     
        }
     
        public void BleServiceOnConnect(string action)
        {
            //當 BLE 連上時想做的事。
        }
     
        public void BleServiceOnDisconnect(string action)
        {
            //當 BLE 斷線時想做的事。
        }
     
        public void BleServiceOnDiscovered(string action)
        {
            //當掃描完 BLE 時想做的事。
        }
     
        public void BleServiceOnDataAvailable(string WriteData, string ReadData)
        {
            //當 BLE 有資料傳入或送出時想做的事。
        }
    }
}
__________________________________________________

檔案:AndroidManifest.xml
第一次編譯 Unity Android 平台時,Unity 專案目錄內會產生 Temp 資料夾。
從 \Unity 專案名稱\Temp\StagingArea\ 複製 AndroidManifest.xml 檔案。
另存到
\Unity 專案名稱\Assets\Plugins\Android\
,如果沒有 Plugins\Android 路徑資料夾則自行建立。

修改 package 名稱為自訂的 Plugin 名稱即可。
__________________________________________________


8 則留言:

PENDRAGON 提到...

感謝大大的教學 這個真的太有用了!!

Unknown 提到...

我最近都在搞Ble, 把mpu6050連arduino再加Ble,用Unity做即時3d互動。不過有點阻滯,想問下你可以sd你的unity project 給我作參考嗎?

My email:lamyinfung3332@gmail.com

Unknown 提到...

您好,最近在學習Unity與藍芽至手機,
有依照上面指示去做練習,但是將Unity Run進手機,會直接閃退結束應用程式,
有發現好像是JavaClass宣告後就會出現這樣的問題,另外將用一個public class 將JavaClass寫成全域變數,
執行後,雖然不會閃退,但是會完全沒有反應,不知道 怎麼改善這情況,
另外還想請教,有特定的UUID,要如何Set呢?

我的 Email:qsqswdwd33@gmail.com

謝謝

Unknown 提到...

您好,我是一名大三的學生
最近在利用Arduino101做一個互動式的遊戲
在藍芽這邊卡關很久
想請問您能否提供您的專案借我們參考學習

Email:p513817@gmail.com

Unknown 提到...

哈囉你好~
想請問一下藍牙部分
找不到
檔案:BleServiceOnReceiveCallback.java
這個腳本,是不是要自己新增?
還有那個MainActivity.java也是嗎

或者有 專案檔可以提供參考 謝謝你
c0975795138@gmail.com

Unknown 提到...

您好,我是一名大三的學生
最近專題在利用ARM lpc1114做一個互動式的遊戲
在藍芽部分困惑很久
請問您是否提供您的專案借我們參考學習

email:jimmy66617@gmail.com

Unknown 提到...

您好,我也是大三在忙研究的學生
我是最近在使用android和arduino感測器
想利用藍牙讓他們溝通
是否能提供您的專案借我們參考學習
不好意思謝謝您

我的email:a0932954941@gmail.com

Unknown 提到...

您好,目前參照您的文章進行實作,但並不順利

請問能否提供專案讓我研究

我的email:rockisshit@gmail.com

謝謝您