Wednesday, 22 March 2017

Bluetooth with Unity for iOS and Android

In recent weeks we've had quite a bit of activity on an old "how to do bluetooth" page on the blog. A lot of people have asked for a zip file or git link so they can just clone it into their projects.

Firstly, that's not quite how this blog works; stuff posted here is a learning aid - probably just an aide-memoire for the few contributors who actually make the stuff in the first place - rather than a repository of open source code. In fact, I get quite resentful of ignorant people who post (often abusive) comments demanding the entire source code project, instead of reading the post and learning how to do it for themselves. It's not a view shared by everyone at Nerd Towers, but for me, any comment beginning "TL;DR" is meaningless drivel.

With that rant out of the way, we recently had need to create a simple bluetooth-enabled app (for a Unity Meetup event next week, demonstrating how to connect Unity/software to hardware/real-world devices). So it seemed like a good time to review the bluetooth Unity library and hopefully clarify a few things that seem to have tripped a few readers up...

The idea here is to simplify using the bluetooth library as much as possible. As the "list all devices and pick one" part of the last post caused such trouble, we've cut it out completely and tried to keep everything together in one place/script. This means we're looking for a specific named device and we'll automatically connect to it, then send and receive data via a couple of text boxes.

Once you've got this working, you should be able to modify the script as necessary to pass values to/from your own functions to make it work in your own Unity project.

Right, first up, import the BTLE library from the Asset Store.


And create a panel in the Unity IDE



Create a new, blank game object then create a script btle_controller.cs. Drag and drop the script onto the blank game object


Create a text object in the panel (this will be our debug window)


Copy and paste this code into the script:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System.Text;

public class btle_controller : MonoBehaviour {

   // -----------------------------------------------------------------
   // change these to match the bluetooth device you're connecting to:
   // -----------------------------------------------------------------
   // private string _FullUID = "713d****-503e-4c75-ba94-3148f18d941e"; // redbear module pattern
   private string _FullUID = "0000****-0000-1000-8000-00805f9b34fb";     // BLE-CC41a module pattern
   private string _serviceUUID = "ffe0";
   private string _readCharacteristicUUID = "ffe1";
   private string _writeCharacteristicUUID = "ffe1";
   private string deviceToConnectTo = "ChrisBLE";

   public bool isConnected=false;
   private bool _readFound=false;
   private bool _writeFound=false;
   private string _connectedID = null;

   private Dictionary<string, string> _peripheralList;
   private float _subscribingTimeout = 0f;

   public Text txtDebug;
   public GameObject uiPanel;
   public Text txtSend;
   public Text txtReceive;
   public Button btnSend;


   // Use this for initialization
   void Start () {
      btnSend.onClick.AddListener (sendData);
      uiPanel.SetActive (false);
      txtDebug.text+="\nInitialising bluetooth \n";
      BluetoothLEHardwareInterface.Initialize (true, false, () => {},
                                    (error) => {}
      );      
      Invoke ("scan", 1f);
   }
   
   // Update is called once per frame
   void Update () {
      if (_readFound && _writeFound) {
         _readFound = false;
         _writeFound = false;
         _subscribingTimeout = 1.0f;
      }

      if (_subscribingTimeout > 0f) {
         _subscribingTimeout -= Time.deltaTime;
         if (_subscribingTimeout <= 0f) {
            _subscribingTimeout = 0f;
            BluetoothLEHardwareInterface.SubscribeCharacteristicWithDeviceAddress (
               _connectedID, FullUUID (_serviceUUID), FullUUID (_readCharacteristicUUID),
               (deviceAddress, notification) => {
               },
               (deviceAddress2, characteristic, data) => {
                  BluetoothLEHardwareInterface.Log ("id: " + _connectedID);
                  if (deviceAddress2.CompareTo (_connectedID) == 0) {
                     BluetoothLEHardwareInterface.Log (string.Format ("data length: {0}", data.Length));
                     if (data.Length == 0) {
                        // do nothing
                     } else {
                        string s = ASCIIEncoding.UTF8.GetString (data);
                        BluetoothLEHardwareInterface.Log ("data: " + s);
                        receiveText (s);
                     }
                  }
            });
            
         }
      }
   }

   void receiveText(string s){
      txtDebug.text += "Received: " + s + " \n";
      txtReceive.text = s;
   }

   void sendDataBluetooth(string sData){
      if (sData.Length > 0) {
         byte[] bytes = ASCIIEncoding.UTF8.GetBytes (sData);
         if (bytes.Length > 0) {
            sendBytesBluetooth (bytes);
         }
      }
   }

   void sendBytesBluetooth(byte[] data){
      BluetoothLEHardwareInterface.Log (string.Format ("data length: {0} uuid {1}", data.Length.ToString (), FullUUID (_writeCharacteristicUUID)));
      BluetoothLEHardwareInterface.WriteCharacteristic (_connectedID, FullUUID(_serviceUUID), FullUUID(_writeCharacteristicUUID),
         data, data.Length, true, (characteristicUUID)=> {
            BluetoothLEHardwareInterface.Log("Write succeeded");      
         }
      );
   }
      

   void scan(){
         
      // the first callback will only get called the first time this device is seen
      // this is because it gets added to a list in the BluetoothDeviceScript
      // after that only the second callback will get called and only if there is
      // advertising data available
      txtDebug.text+=("Starting scan \r\n");
      BluetoothLEHardwareInterface.ScanForPeripheralsWithServices (null, (address, name) => {
         AddPeripheral (name, address);
      }, (address, name, rssi, advertisingInfo) => {});
                                     
   }

   void AddPeripheral (string name, string address){
      txtDebug.text+=("Found "+name+" \r\n");

      if (_peripheralList == null) {
         _peripheralList = new Dictionary<string, string> ();
      }
      if (!_peripheralList.ContainsKey (address)) {
         _peripheralList [address] = name;
         if (name.Trim().ToLower() == deviceToConnectTo.Trim().ToLower()) {
            //txtDebug.text += "Found our device, stop scanning \n";
            //BluetoothLEHardwareInterface.StopScan ();

            txtDebug.text += "Connecting to " + address + "\n";
            connectBluetooth (address);
         } else {
            txtDebug.text += "Not what we're looking for \n";
         }
      } else {
         txtDebug.text += "No address found \n";
      }
   }

   void connectBluetooth(string addr){
      BluetoothLEHardwareInterface.ConnectToPeripheral (addr, (address) => {
      },
         (address, serviceUUID) => {
         },
         (address, serviceUUID, characteristicUUID) => {
                     
            // discovered characteristic
            if (IsEqual (serviceUUID, _serviceUUID)) {
               _connectedID = address;         
               isConnected = true;

               if (IsEqual (characteristicUUID, _readCharacteristicUUID)) {
                  _readFound = true;
               }
               if (IsEqual (characteristicUUID, _writeCharacteristicUUID)) {
                  _writeFound = true;
               }
                  
               txtDebug.text += "Connected";
               BluetoothLEHardwareInterface.StopScan ();
               uiPanel.SetActive (true);

            }
         }, (address) => {

            // this will get called when the device disconnects
            // be aware that this will also get called when the disconnect
            // is called above. both methods get call for the same action
            // this is for backwards compatibility
            isConnected = false;
         });
               
   }


   void sendData(){
      string s = txtSend.text.ToString ();
      sendDataBluetooth (s);
   }

   // -------------------------------------------------------
   // some helper functions for handling connection strings
   // -------------------------------------------------------
   string FullUUID (string uuid) {
      return _FullUID.Replace ("****", uuid);
   }

   bool IsEqual(string uuid1, string uuid2){
      if (uuid1.Length == 4) {
         uuid1 = FullUUID (uuid1);
      }
      if (uuid2.Length == 4) {
         uuid2 = FullUUID (uuid2);
      }
      return (uuid1.ToUpper().CompareTo(uuid2.ToUpper()) == 0);
   }

}


... and hook up all the controls to the public variables. Starting with the debug text object, drag and drop this into the public variable slot in the Unity IDE.


Now create a second panel (we made ours dark so you can see it clearly) and link this up to the script variable by dragging and dropping it in the Unity IDE.


Create an input object (which we'll type in to send data) and a text object, as children of the second panel (this panel gets disabled until the Bluetooth device has connected). Plop a button on there too while you're about it. As before, hook up all the on-screen game objects to the script by dragging and dropping.


We tested our project using Unity Remote on an LG G3 phone and a simple Arduino/BLE-CC41a combo (pushing data onto the serial port to send/receive over bluetooth).

IMPORTANT:
If you're running the BTLE4 library on a device running Android 6.0 or later, you'll need to add an extra line to the AndroidManifest.xml file


<uses-permission: android:name="android.permission.ACCESS_COARSE_LOCATION" />


Now fire up your app (we found it only worked on an actual device, running it in test-mode on the PC did nothing- even with bluetooth enabled on the laptop) and send sending/receiving data over bluetooth!


Note:
If you're getting nothing, and no errors during compile, it's quite likely that the UUID patterns are incorrect. Use some software such as BLEGattList (for Android) on your device to query not only which bluetooth devices are available, but also which UUIDs they are using.

We found that the bottom device, listed as "unknown service" beginning 0000ffe0 with a single read/write characteristic with the leading digits 0000ffe1 gave us the values to "plug in" to our code: the service was FFE0, the read characteristic FFE1 and the write characteristic was also FFE1.


This seems pretty standard across almost every one of these cheap bluetooth modules that mimic the BLE-CC41a devices. We did find an old RedBear bluetooth module which we got working, but the FullUID string had to be changed to something completely unrecognisable, and the read and write characteristics had different values (from memory something like 0002 for reading and 0001 for writing).

Anyway, there it is - very much a cut-down, quick and dirty way of getting your device to talk to bluetooth modules, all from a single script. Please don't ask for zip files or full source code - the BTLE plugin is available on the asset store, it costs about a tenner and is well worth spending a few quid to support a fellow programmer. With that installed you can copy and paste a single script, assign some variables and off you go!