BLE Connection to Kidbright32 - fllay/raspberrypiBLEesp32 GitHub Wiki

To create communication between a raspberry pi (e.g. Kidbright AI bot, box) and a Kidbright32 board, we can use GATT BLE since it does not require paring process. It can only use MAC address and UUIDs to establish a link between the raspberry pi and the Kidbright32 board.

BLE protocol stack is shown below.

alt text BLE Protocol stack

The process to establish the BLE link is shown below. When the peripheral is turn on, it will start advitise (broadcast) the services. Then, the central device (Raspberry pi) will scan for BLE broadcasting signal and it will see all BLE device name and BLE MAC address. Once the desire device name is found, we can make a connection by using the corresponding BLE MAC address. We can the check all available service UUIDs. Characteristics whithin the service are where the data is stored. They act like variables for exchnaging data. There are three types of commands to exchange data between peripherals and the central. The write command is used for sending data from central to peripheral. The read command is used for central to get data from server on-demand. The server can push data to the client by using either notify or indicate command. notify command will be used when acknownlagment is not required where as notify command is used when the client can acknownlage for received data.

alt text

Let us start with ESP32 (Kidbright32 board) code. The following scketch will create a BLE server on Kidbright32 board. User can send data using serial command on Ardiono IDE serial terminal.

ESP32 Arduino code for BLE server

/*
    Video: https://www.youtube.com/watch?v=oCMOYS71NIU
    Based on Neil Kolban example for IDF: https://github.com/nkolban/esp32-snippets/blob/master/cpp_utils/tests/BLE%20Tests/SampleNotify.cpp
    Ported to Arduino ESP32 by Evandro Copercini

   Create a BLE server that, once we receive a connection, will send periodic notifications.
   The service advertises itself as: 6E400001-B5A3-F393-E0A9-E50E24DCCA9E
   Has a characteristic of: 6E400002-B5A3-F393-E0A9-E50E24DCCA9E - used for receiving data with "WRITE" 
   Has a characteristic of: 6E400003-B5A3-F393-E0A9-E50E24DCCA9E - used to send data with  "NOTIFY"

   The design of creating the BLE server is:
   1. Create a BLE Server
   2. Create a BLE Service
   3. Create a BLE Characteristic on the Service
   4. Create a BLE Descriptor on the characteristic
   5. Start the service.
   6. Start advertising.

   In this example rxValue is the data received (only accessible inside that function).
   And txValue is the data to be sent, in this example just a byte incremented every second. 
*/
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#include "esp_bt_device.h"#include "esp_bt_device.h"

BLEServer *pServer = NULL;
BLECharacteristic * pTxCharacteristic;
bool deviceConnected = false;
bool oldDeviceConnected = false;
uint8_t txValue = 0;

// See the following for generating UUIDs:
// https://www.uuidgenerator.net/

#define SERVICE_UUID           "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" // UART service UUID
#define CHARACTERISTIC_UUID_RX "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
#define CHARACTERISTIC_UUID_TX "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"


void printDeviceAddress() {
 
  const uint8_t* point = esp_bt_dev_get_address();
 
  for (int i = 0; i < 6; i++) {
 
    char str[3];
 
    sprintf(str, "%02X", (int)point[i]);
    Serial.print(str);
 
    if (i < 5){
      Serial.print(":");
    }
 
  }
}

class MyServerCallbacks: public BLEServerCallbacks {
    void onConnect(BLEServer* pServer) {
      deviceConnected = true;
    };

    void onDisconnect(BLEServer* pServer) {
      deviceConnected = false;
    }
};

class MyCallbacks: public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic *pCharacteristic) {
      std::string rxValue = pCharacteristic->getValue();

      if (rxValue.length() > 0) {
        Serial.println("*********");
        Serial.print("Received Value: ");
        for (int i = 0; i < rxValue.length(); i++)
          Serial.print(rxValue[i]);

        Serial.println();
        Serial.println("*********");
      }
    }
};


void setup() {
  Serial.begin(115200);

  // Create the BLE Device
  BLEDevice::init("UART Service");

  // Create the BLE Server
  pServer = BLEDevice::createServer();
  pServer->setCallbacks(new MyServerCallbacks());

  // Create the BLE Service
  BLEService *pService = pServer->createService(SERVICE_UUID);

  // Create a BLE Characteristic
  pTxCharacteristic = pService->createCharacteristic(
                    CHARACTERISTIC_UUID_TX,
                    BLECharacteristic::PROPERTY_NOTIFY
                  );
                      
  pTxCharacteristic->addDescriptor(new BLE2902());

  BLECharacteristic * pRxCharacteristic = pService->createCharacteristic(
                       CHARACTERISTIC_UUID_RX,
                      BLECharacteristic::PROPERTY_WRITE
                    );

  pRxCharacteristic->setCallbacks(new MyCallbacks());

  // Start the service
  pService->start();

  // Start advertising
  pServer->getAdvertising()->start();
  Serial.print("BT MAC: ");
  printDeviceAddress();
  Serial.println("");
  Serial.println("Waiting a client connection to notify...");
}

void loop() {

    /*if (deviceConnected) {
        pTxCharacteristic->setValue(&txValue, 1);
        pTxCharacteristic->notify();
        txValue++;
    delay(1000); // bluetooth stack will go into congestion, if too many packets are sent
  }*/
  
  if (deviceConnected) {
    if (Serial.available()){
      char ch = Serial.read();
      txValue = (uint8_t) ch;
      pTxCharacteristic->setValue(&txValue, 1);
      pTxCharacteristic->notify();
    }
  }

    // disconnecting
    if (!deviceConnected && oldDeviceConnected) {
        delay(500); // give the bluetooth stack the chance to get things ready
        pServer->startAdvertising(); // restart advertising
        Serial.println("start advertising");
        oldDeviceConnected = deviceConnected;
    }
    // connecting
    if (deviceConnected && !oldDeviceConnected) {
    // do stuff here on connecting
        oldDeviceConnected = deviceConnected;
    }
}

After BLE server starts, the serial terninal will show that it is waiting for connection. It can be seen that the BLE MAC address is also shown in the terminal. This MAC address will be used later in python code for making a connection. In this example, the MAC address is 8C:AA:B5:8C:B7:1A

alt text

Now we are readly for python code on Raspbery pi. The required package, bluepy , can ne install by using command sudo pip3 install bluepy. The output is somewhat similar to the following

pi@KidBrightAI02:~$ sudo pip3 install bluepy
WARNING: The directory '/home/pi/.cache/pip' or its parent directory is not owned or is not writable by the current user. The cache has been disabled. Check the permissions and owner of that directory. If executing pip with sudo, you may want sudo's -H flag.
Collecting bluepy
  Downloading bluepy-1.3.0.tar.gz (217 kB)
     |████████████████████████████████| 217 kB 1.8 MB/s 
Building wheels for collected packages: bluepy
  Building wheel for bluepy (setup.py) ... done
  Created wheel for bluepy: filename=bluepy-1.3.0-cp36-cp36m-linux_aarch64.whl size=563418 sha256=0c3289f178a427d8ab76230ac7097d46e77915b56df5ea338bc6b05b6cf35d92
  Stored in directory: /tmp/pip-ephem-wheel-cache-gxuwz1_h/wheels/16/43/3b/dbf93f0d5e1fb36c82d1632ae26d1e921e2a6d898f05411a58
Successfully built bluepy
Installing collected packages: bluepy
Successfully installed bluepy-1.3.0
WARNING: You are using pip version 20.3.1; however, version 21.3.1 is available.
You should consider upgrading via the '/usr/bin/python3 -m pip install --upgrade pip' command.

We start with scanning code to see all BLE devices around us. Let create a bluesman.py

from bluepy.btle import Scanner, DefaultDelegate

class ScanDelegate(DefaultDelegate):
    def __init__(self):
        DefaultDelegate.__init__(self)

    def handleDiscovery(self, dev, isNewDev, isNewData):
        if isNewDev:
            print("Discovered device", dev.addr)
        elif isNewData:
            print("Received new data from", dev.addr)

scanner = Scanner().withDelegate(ScanDelegate())
devices = scanner.scan(10.0)

for dev in devices:
    print("Device {} ({}), RSSI={} dB".format(dev.addr, dev.addrType, dev.rssi))
    for (adtype, desc, value) in dev.getScanData():
        print("  {} = {}".format(desc, value))

We can then execute code to see the BLE devices. Note that we need sudo to run this command.

pi@KidBrightAI02:~/python$ sudo python3 blescan.py 
Discovered device 8c:aa:b5:8c:b7:1a
Discovered device 50:34:fc:14:f5:38
Discovered device 1c:3c:b1:a9:55:e6
Discovered device 6c:14:4b:b4:81:63
Discovered device 36:ba:92:c1:e5:e5
Discovered device d0:03:4b:e6:b6:8a
Discovered device e8:e3:74:b8:dc:34
Discovered device ff:b3:dd:32:e4:20
Device 8c:aa:b5:8c:b7:1a (public), RSSI=-104 dB
  Flags = 06
  Complete Local Name = UART Service
  Manufacturer = 12234556
  Tx Power = eb
Device 50:34:fc:14:f5:38 (random), RSSI=-106 dB
  Flags = 1a
  Tx Power = 0c
  Manufacturer = 4c0010070a1fa9aec2ad58
Device 1c:3c:b1:a9:55:e6 (random), RSSI=-108 dB
  Flags = 1a
  Manufacturer = 4c0009060305c0a8440c
Device 6c:14:4b:b4:81:63 (random), RSSI=-101 dB
  Flags = 1a
  Manufacturer = 4c000c0e08bfa6cd7055a7c69e3d67ca384210064a1d7edfba08
Device 36:ba:92:c1:e5:e5 (random), RSSI=-108 dB
  Flags = 1a
  Manufacturer = 4c0009060337c0a84409
Device d0:03:4b:e6:b6:8a (public), RSSI=-107 dB
  Flags = 1a
  Tx Power = 0c
  Manufacturer = 4c0010050814357564
Device e8:e3:74:b8:dc:34 (random), RSSI=-105 dB
  Manufacturer = 4c0012020001
Device ff:b3:dd:32:e4:20 (random), RSSI=-104 dB
  Manufacturer = 4c0012020003

We will see a BLE device name UART Service and the corresponding BLE MAC address 8c:aa:b5:8c:b7:1a. Next we will create a BLE client to connect to this device using BLE MAC address of 8c:aa:b5:8c:b7:1a.

To create BLE client on the Raspberry pi, we need to set the following values for connection.

  • BLE MAC Address
  • Service UUID
  • Characteristic UUIDs

These values need to be matched with the values in Arduino code. For this example,

  • BLE MAC Address is 8C:AA:B5:8C:B7:1A
  • Service UUID is 6E400001-B5A3-F393-E0A9-E50E24DCCA9E
  • Characteristic UUIDs are 6E400002-B5A3-F393-E0A9-E50E24DCCA9E, 6E400003-B5A3-F393-E0A9-E50E24DCCA9E Hence, python code for BLE connection initialization is
p = btle.Peripheral("8C:AA:B5:8C:B7:1A")   

# Setup to turn notifications on, e.g.
svc = p.getServiceByUUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
ch_Tx = svc.getCharacteristics("6E400002-B5A3-F393-E0A9-E50E24DCCA9E")[0]
ch_Rx = svc.getCharacteristics("6E400003-B5A3-F393-E0A9-E50E24DCCA9E")[0]

Complete python code is given below.

from bluepy import btle
import time

class MyDelegate(btle.DefaultDelegate):
    def __init__(self):
        btle.DefaultDelegate.__init__(self)
        # ... initialise here

    def handleNotification(self, cHandle, data):
        #print("\n- handleNotification -\n")
        print(data)
        # ... perhaps check cHandle
        # ... process 'data'

# Initialisation  -------

def bleConnect(addr):
    p = btle.Peripheral(addr)   #NodeMCU-32S
    # Setup to turn notifications on, e.g.
    svc = p.getServiceByUUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
    p.setDelegate( MyDelegate())
    return svc
    

addr = "80:7D:3A:FC:57:52"
svc = bleConnect(addr)
ch_Tx = svc.getCharacteristics("6E400002-B5A3-F393-E0A9-E50E24DCCA9E")[0]
ch_Rx = svc.getCharacteristics("6E400003-B5A3-F393-E0A9-E50E24DCCA9E")[0]
lasttime = time.localtime()
while True:
    """
    if p.waitForNotifications(1.0):
        pass  #continue

    print("Waiting...")
    """
    
    nowtime = time.localtime()
    if(nowtime > lasttime):
        lasttime = nowtime
        stringtime = time.strftime("%H:%M:%S", nowtime)
        btime = bytes(stringtime, 'utf-8')
        try:
            ch_Tx.write(btime, True)
        except btle.BTLEException:
            
            print("btle.BTLEException1");
            while True:
                try:
                    print("trying to reconnect with "  )
                    svc = bleConnect(addr)
                    ch_Tx = svc.getCharacteristics("6E400002-B5A3-F393-E0A9-E50E24DCCA9E")[0]
                    ch_Rx = svc.getCharacteristics("6E400003-B5A3-F393-E0A9-E50E24DCCA9E")[0]
                 
                    print("re-connected to ")
                    
                    time.sleep(1)
                    break
                except:
                    continue

This code will send date-time to the BLE peripheral (Kidbright32 board) every 1 second. Once run this code at the terminal.

pi@KidBrightAI02:~/python$ python3 bleuart.py

The serial monitor will show somthing like this.

alt text

Data can be sent to BLE client (Raspberry pi) by using the serial terminal. For example, if we put "Eru" in the serial terminal and click Send.

alt text

At the terminal, we will see the follwing.

pi@KidBrightAI02:~/python$ python3 bleuart.py
b'E'
b'r'
b'u'
b'\n'

To use BLE with ROS node.

#!/usr/bin/env python3
import rospy
from std_msgs.msg import String
from kidbright_tpu.msg import tpu_objects
from bluepy import btle
import time

class MyDelegate(btle.DefaultDelegate):
    def __init__(self):
        btle.DefaultDelegate.__init__(self)
        # ... initialise here

    def handleNotification(self, cHandle, data):
        #print("\n- handleNotification -\n")
        print(data)
        # ... perhaps check cHandle
        # ... process 'data'

# Initialisation  -------

def bleConnect(addr):
    p = btle.Peripheral(addr)   #NodeMCU-32S
    # Setup to turn notifications on, e.g.
    svc = p.getServiceByUUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
    p.setDelegate( MyDelegate())
    return svc
    

addr = "9C:9C:1F:C5:BF:BA"
svc = bleConnect(addr)
ch_Tx = svc.getCharacteristics("6E400002-B5A3-F393-E0A9-E50E24DCCA9E")[0]
ch_Rx = svc.getCharacteristics("6E400003-B5A3-F393-E0A9-E50E24DCCA9E")[0]


def callback(data):
    global ch_Tx, ch_Rx, svc
    #print(data.tpu_objects)
    for obj in data.tpu_objects:
        #rospy.loginfo(rospy.get_caller_id() + "I heard %s", obj.label)
        try:
            bleData = bytes(obj.label, 'utf-8')
            ch_Tx.write(bleData, True)
        except btle.BTLEException:
            
            print("btle.BTLEException1");
            while True:
                try:
                    print("trying to reconnect with "  )
                    svc = bleConnect(addr)
                    ch_Tx = svc.getCharacteristics("6E400002-B5A3-F393-E0A9-E50E24DCCA9E")[0]
                    ch_Rx = svc.getCharacteristics("6E400003-B5A3-F393-E0A9-E50E24DCCA9E")[0]
                 
                    print("re-connected to ")
                    
                    time.sleep(1)
                    break
                except:
                    continue
    
def listener():

    # In ROS, nodes are uniquely named. If two nodes with the same
    # name are launched, the previous one is kicked off. The
    # anonymous=True flag means that rospy will choose a unique
    # name for our 'listener' node so that multiple listeners can
    # run simultaneously.
    
    rospy.init_node('listener', anonymous=True)

    rospy.Subscriber("/tpu_objects", tpu_objects, callback)

    # spin() simply keeps python from exiting until this node is stopped
    rospy.spin()

if __name__ == '__main__':
    listener()

To establish BLE connections to multiple KidBright boards, we need to create a python file for each KidBright board and each python python will be executed concurrently on the Raspberry pi. alt text

#!/usr/bin/env python3
import rospy
from std_msgs.msg import String
from kidbright_tpu.msg import tpu_objects
from bluepy import btle
import time
import Queue

q_nav = Queue.Queue()

class MyDelegate(btle.DefaultDelegate):
    def __init__(self):
        btle.DefaultDelegate.__init__(self)
        # ... initialise here

    def handleNotification(self, cHandle, data):
        #print("\n- handleNotification -\n")
        print(data)
        # ... perhaps check cHandle
        # ... process 'data'

# Initialisation  -------

def bleConnect(addr):
    p = btle.Peripheral(addr)   #NodeMCU-32S
    # Setup to turn notifications on, e.g.
    svc = p.getServiceByUUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
    p.setDelegate( MyDelegate())
    return svc
    

addr = "9C:9C:1F:C5:BF:BA"
svc = bleConnect(addr)
ch_Tx = svc.getCharacteristics("6E400002-B5A3-F393-E0A9-E50E24DCCA9E")[0]
ch_Rx = svc.getCharacteristics("6E400003-B5A3-F393-E0A9-E50E24DCCA9E")[0]


def callback(data):
    global ch_Tx, ch_Rx, svc
    #print(data.tpu_objects)
    q_nav.put(data.tpu_objects)

    
def listener():

    # In ROS, nodes are uniquely named. If two nodes with the same
    # name are launched, the previous one is kicked off. The
    # anonymous=True flag means that rospy will choose a unique
    # name for our 'listener' node so that multiple listeners can
    # run simultaneously.
    
    rospy.init_node('listener', anonymous=True)

    rospy.Subscriber("/tpu_objects", tpu_objects, callback)
    rate = rospy.Rate(10.0)

    # spin() simply keeps python from exiting until this node is stopped
    while not rospy.is_shutdown():
        if not q_nav.empty():
            tpu_objects =  q_nav.get()
            for obj in tpu_objects:
                #rospy.loginfo(rospy.get_caller_id() + "I heard %s", obj.label)
                try:
                    bleData = bytes(obj.label, 'utf-8')
                    ch_Tx.write(bleData, True)
                except btle.BTLEException:
            
                    print("btle.BTLEException1");
                    while True:
                        try:
                            print("trying to reconnect with "  )
                            svc = bleConnect(addr)
                            ch_Tx = svc.getCharacteristics("6E400002-B5A3-F393-E0A9-E50E24DCCA9E")[0]
                            ch_Rx = svc.getCharacteristics("6E400003-B5A3-F393-E0A9-E50E24DCCA9E")[0]
                 
                            print("re-connected to ")
                    
                            time.sleep(1)
                            break
                        except:
                            continue
        rate.sleep()


if __name__ == '__main__':
    listener()
⚠️ **GitHub.com Fallback** ⚠️