Friday, 22 January 2016

Talking to Bluetooth LE devices with Unity 5

In recent months, a few of us have been playing about with Unity. Steve is the master of playing about with stuff. He takes a prefab or shop-bought asset and kicks it about until it just does what he insists it should, through sheer dogged determined willpower. Scropp, like me, takes a slower, more in-depth view of how it works; maybe taking a bit longer to get there, but ultimately having a deeper understanding of how it all works.

That's not to say that either approach is correct, nor that any approach is wrong.
But after playing about with an existing BTLE plug-in from the Asset store(https://www.assetstore.unity3d.com/en/#!/content/26661) I was getting frustrated at being unable to "hack" the existing demo project to do what I wanted it to.


Firstly, it's written in about a million pieces, with classes referencing classes, and references to other Unity scripts (this is a bit of a weird idea for me - where a variable is declared globally, but not actually assigned in code, and simply drag-and-dropped into place using the Unity visual editor).

Secondly, the example script is difficult to follow easily. There are loads of panels and assets that are clearly of no use for most projects (they're included to demonstrate how flexible the plug-in can be with different types of hardware, but you'd never use all of them in the same project). But simply removing assets you don't want causes references to scripts that may or may not be needed to break, and the script stops compiling.

Trying to trace these back and find which reference to which no-longer-needed bit (especially when the reference is placed in the Unity editor and doesn't actually appear in the code anyway) burned a whole day. Sometimes hacking Unity example code can feel a little bit like using Arduino libraries.
With enough perseverance, you can get something to work. But that's about it - really getting it to do what you want instead of having to put up with how it works is about the best you can hope for. I didn't like that idea with my Unity project, so was determined to use the core library for the bluetooth communication, but build my own interface, from a blank Unity Project.

Here's how I did it.

Firstly, import the bluetooth LE library (for iOS and Android - let's build to multiple platforms from the same source code!) Then go into the assets folder and get rid of pretty much most of it!


I kept the plugins folder intact and got rid of everything else. In the screenshot above, the sprites folder remains, but it doesn't actually contain anything! The prefabs folder contains my own prefab (explained later) and the original prefab that came with the demo has been removed. Basically get rid of everything except the contents of the plugins folder.

I created two panels - one shall be the screen when the app first starts, listing all the available bluetooth devices in range, then when one is selected, the app will connect and display the "connected" panel (which is a simple interface for us to send and receive strings of text but you can make yours do pretty much anything you like!)

In the editor they are placed over the top of each other, but in the screenshot have been separated out so you can see the entire hierarchy. I tend to make my panels obviously different colours during development so that you can see which panel is supposed to be showing at any one time.


Unlike the original demo, we're going to shove everything into a single class. It's not exactly perfect programming practice, but this is a single-developer project, not something that's going to be shared as a library, or developed in tandem with fifteen other developers; my personal feeling is that a multi-classed development approach too early on just over-complicates things, when a few simple function calls and simple procedural processing can help make things much easier to understand (which is really useful when you come back to this in six months and don't have to re-learn how it works all over again!)

A controller script is created and attached to the canvas object. For new Unity users, it's as simple as right-clicking in the project and selecting "new script" and giving it a name, then dragging and dropping it onto the canvas object in the hierarchy view on the left. I called my script, rather cryptically, "controller.cs".

In the start function, simply create a couple of global references to the "scanning" and "connected" panels. Maybe it's a throwback to my embedded development time, but I just find global variables much easier to work with that the constant swapping of references and passing parameters that a fully object-oriented approach requires. Don't listen to what your university lecturer tells you - in the real world, we use global variables. A lot.

Then simply initialise the bluetooth plugin with a single line of code:

BluetoothLEHardwareInterface.Initialize (true, false, () => {}, (error) => {});

And then invoke our scanning routine after about one second (which gives the bluetooth plugin time to initialise properly) using

Invoke("scan",1000);

This simply tells Unity to "run the function called -scan- after one second".
The scan function monitors the bluetooth connection and when a device is within range, it calls the AddPeripheral function, to make the device appear in a list, as a clickable (button) object.

And this is where Unity mixes its programmatic and visual approaches. Personally, I don't like it. But some people love it, so this is how it goes:

The AddPeripheral function creates an instance of a button "prefab" and attaches to a (scrollable) panel. But we don't just say "use this panel" but create a global variable in which we store a reference to the scrollable panel object. Strangely, Unity likes this to be set using the visual editor, rather than entirely programmatically. So we create a global variable, so that the editor can access (and amend) the variable and when the script object is viewed in the editor, we can populate all the public variable values.

In fact we had to create a number of global variables to act as references to objects already in our Unity project

// --------------------------------------------------------
// these objects need to be set in the Unity visual editor
// so they are actually linked to something!
// --------------------------------------------------------
public Transform PanelScrollContents; // the panel on which you'll list the devices
public Text txtDebug; // this is just for testing, can be removed safely
public GameObject connectButton; // the button to click to connect to a device
public Text txtData; // the text box to type in your "send" data
public Text txtReceive; // the text box data is received into (for testing)

And then in the visual editor, linked them to the objects on screen



The last thing to do is create our "device connect" button as a prefab, so that it can be created at runtime in code. To create a prefab in Unity is pretty easy. Create your object on the screen in the main editor window and when you're happy with it, drag-n-drop it into the project space (the panel at the bottom in the middle of the screen) and Unity creates a "prefab" version of the object.

Our button has a very simple script attached to it.
It allows us to set the label of the button and also to retrieve the (mac) address of the device to connect to, when the button is pressed.

using UnityEngine;
using UnityEngine.UI;

public class connectButtonScript : MonoBehaviour {

     public controller controllerScript;
     public Text TextName;
     public Text TextAddress;

     public void onClick () {
          controllerScript.connectTo (TextName.text, TextAddress.text);
     }

     // Use this for initialization
     void Start () {
     
     }
     
     // Update is called once per frame
     void Update () {
     
     }
}

Although it's very simple we have to create three global references.
One is a variable onto which we drop the text label on the button. Another is a (transparent/hidden) text label which we use to store the address. And then we have to store a reference to the script/class that has created/called this one. For over twenty years, I've always hated this approach to programming. But here it is. In the AddPeripheral function, you'll see something like this:

GameObject buttonObject = (GameObject)Instantiate (connectButton);
connectButtonScript script = buttonObject.GetComponent ();
script.TextName.text = name;
script.TextAddress.text = address;
script.controllerScript = this;

That's creating an instance of our button prefab, and saying set the .text property of the global variable TextName to whatever the name of the object you've just clicked on is. The global variable TextName appears in the Unity visual editor and you have to drag and drop the "name" label from the button into the TextName global variable. A horrible way of completing references, but one that works, so let's stick with it for now!

Similarly the global variable TextAddress that was declared in the connectButtonScript.cs file is populated in the Unity visual editor by dragging and dropping the "address" label from the button onto the field.

The controllerScript global variable holds a reference to the script/class that created the button (so when the button is clicked, it can make a callback to the script that created it). In our case, a simple "this" will do - the controller.cs class is simply saying to the buttonScript "I am the controller script that created you".

And with all that in mind, here's the full controller.cs script

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

public class controller : MonoBehaviour {

   // -------------------------------------------------------------------
   // this project doesn't offer a "scan" and "connect" buttons - it
   // just gets on and does it!
   // So when it runs, it immediately starts scanning and lists all
   // found devices in a list. Click the one you want and it immediately
   // connects without the need to click a "connect" button
   // -------------------------------------------------------------------

   // -----------------------------------------------------------------
   // change these to match the bluetooth device you're connecting to:
   // -----------------------------------------------------------------
   private string _FullUID = "713d****-503e-4c75-ba94-3148f18d941e";
   private string _serviceUUID = "0000";             
   private string _readCharacteristicUUID = "0002";    
   private string _writeCharacteristicUUID = "0003";

   // ---------------------------------------------------------------------
   // if you want to connect to different devices, you *could* take a look
   // at the function [void connectTo] and add in some if statements to
   // change the FullUID pattern to match the device
   // ---------------------------------------------------------------------



   // ----------------------------------------------------------------------
   // the following are public because they need to be set by dragging and
   // dropping the various objects in the Unity editor
   // ----------------------------------------------------------------------
   // --------------------------------------------------------
   // these objects need to be set in the Unity visual editor
   // so they are actually linked to something!
   // --------------------------------------------------------
   public Transform PanelScrollContents;            // the panel on which you'll list the devices
   public Text txtDebug;                        // this is just for testing, can be removed safely
   public GameObject connectButton;               // the button to click to connect to a device
   public Text txtData;                        // the text box to type in your "send" data
   public Text txtReceive;                        // the text box data is received into (for testing)


   // -------------------------------------------------------------
   // leave the rest of this junk alone but edit the couple
   // of functions that send and receive data if you like:
   //
   // sendDataBluetooth('string to send');
   //
   // and when data is received, it's passed into the function:
   //
   // receiveText('string received over bluetooth');
   //
   // so change these functions to do whatever you want them to do
   // and leave all the other stuff alone and it should just work!
   // -------------------------------------------------------------

   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;
   private bool _scanning = false;
   private bool _connecting = false;

   private int devicesFound = 0;

   private GameObject panelScan;
   private GameObject panelConnected;
   private GameObject panelSettings;
   

   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;
            } else if (IsEqual (characteristicUUID, _writeCharacteristicUUID)) {
               _writeFound = true;
            }

            showConnected();
         }
      }, (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;
      });

      _connecting = false;
   }
   
   string FullUUID (string uuid) {
      // this has changed for the BTLE Mini devices
      // return "713d" + uuid + "-503e-4c75-ba94-3148f18d941e";
      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);
   }

   public void connectTo(string sName, string sAddress){
      if (_connecting == false) {
         txtDebug.text += "Connect to " + sName + " " + sAddress + "\n";
         _connecting=true;

         // stop scanning
         BluetoothLEHardwareInterface.StopScan ();
         _scanning = false;

         // connect to selected device
         connectBluetooth (sAddress);
      }
   }

   public void scan(){
      if (_scanning==true) {
         txtDebug.text+="Stop scan\n";
         BluetoothLEHardwareInterface.StopScan ();
         _scanning = false;
      } else {

         txtDebug.text+="Start scan\n";
         RemovePeripherals ();
            
         devicesFound=0;

         // 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
         BluetoothLEHardwareInterface.ScanForPeripheralsWithServices (null, (address, name) => {               
            AddPeripheral (name, address);               
         }, (address, name, rssi, advertisingInfo) => {});
            
         _scanning = true;
      }         
   }


   void RemovePeripherals () {
      for (int i = 0; i < PanelScrollContents.childCount; ++i) {
         GameObject gameObject = PanelScrollContents.GetChild (i).gameObject;
         Destroy (gameObject);
      }
      
      if (_peripheralList != null) {
         _peripheralList.Clear ();
      }
   }
   
   void AddPeripheral (string name, string address){
      if (_peripheralList == null) {
         _peripheralList = new Dictionary<string, string> ();
      }
      if (!_peripheralList.ContainsKey (address)) {

         txtDebug.text+="Found "+name+"\n";
         devicesFound++;

         GameObject buttonObject = (GameObject)Instantiate (connectButton);
         connectButtonScript script = buttonObject.GetComponent<connectButtonScript> ();
         script.TextName.text = name;
         script.TextAddress.text = address;
         script.controllerScript = this;

         // each button is 50 pixels high
         // the container panel is 544 pixels high
         var h = (544 / 2) - (55*devicesFound);

         buttonObject.transform.SetParent (PanelScrollContents);
         buttonObject.transform.localScale = new Vector3 (1f, 1f, 1f);   
         buttonObject.transform.localPosition = new Vector3 (0, h, 0);

         _peripheralList[address] = name;

         txtDebug.text+="Button created\n";
      }
   }

   
   void sendByte (byte value) {
      byte[] data = new byte[] { value };
      BluetoothLEHardwareInterface.WriteCharacteristic (_connectedID, FullUUID (_serviceUUID), FullUUID (_writeCharacteristicUUID), data, data.Length, true, (characteristicUUID) => {         
         BluetoothLEHardwareInterface.Log ("Write Succeeded");
      });
   }
   
   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 sendDataBluetooth(string sData){
      if (sData.Length > 0) {
         byte[] bytes = ASCIIEncoding.UTF8.GetBytes (sData);
         if (bytes.Length > 0) {
            sendBytesBluetooth (bytes);
         }      
      }
   }

   void receiveText(string s){
      txtReceive.text += s;
   }

   public void clearReceived(){
      txtReceive.text = "";
   }

   public void sendBluetooth(){
      string sData = txtData.text;
      txtDebug.text+="Sending: "+sData+"\n";
      sendDataBluetooth (sData);
      txtDebug.text += "Sent";
   }

   void disconnect (Action<string> action){
      BluetoothLEHardwareInterface.DisconnectPeripheral (_connectedID, action);
   }

   void showScan(){
      panelSettings.SetActive (false);
      panelConnected.SetActive (false);
      panelScan.SetActive (true);
   }

   void showConnected(){
      panelSettings.SetActive (false);
      panelScan.SetActive (false);
      panelConnected.SetActive (true);
   }

   void showSettings(){
      panelScan.SetActive (false);
      panelConnected.SetActive (false);
      panelSettings.SetActive (true);
   }

   void Initialise(){
      BluetoothLEHardwareInterface.Initialize (true, false, () => {}, (error) => {});
   }
   
   // Use this for initialization
   void Start () {

      panelScan = GameObject.Find ("panelScan");
      panelSettings = GameObject.Find ("panelSettings");
      panelConnected = GameObject.Find ("panelConnected");

      // set up the panels
      showScan ();

      // initialise the bluetooth library
      Initialise ();

      // start scanning after 1 second
      Invoke("scan",1000);
   }



   // Update is called once per frame
   void Update () {
   
      if (_readFound && _writeFound){
         _readFound = false;
         _writeFound = false;         
         _subscribingTimeout = 1f;
      }
      
      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);
                  }
               }
               
            });

         }

      }

   }
}

2 comments:

  1. Hello I am a but confused on the btnDevice prefab you made and how the connectButtonScript fits into this. I know you showed you hierarchy here but there are some gaps in your process, I would really appreciate some help if possible.

    ReplyDelete
  2. Hi, I have a few questions about this project, and was hoping to get in touch with you about it. I’m attempting to create a bluetooth controller for a Google Cardboard game, and would appreciate any help!

    ReplyDelete