# Turn BLE: Implementing BLE Sensors with MCU Devkits

In the first episode of this series, I shared with you the project that I plan to implement.  I went through the initial planning and presented a selection of MCU devkit boards that would be suitable for our purposes.

In this episode, I will try and implement BLE communication on one of the boards.  Since the idea is to implement
this project as if it were a proof of concept (PoC), once I am moderately successful with one implementation, I will stop there and move forward to the next step, which is implementing the BLE central role in the Raspberry Pi.

Are you ready? Then buckle up for the Bluetooth bumps ahead!

# Table of Contents

1.  Concepts
    1.  Bluetooth classic vs BLE
    2.  BLE data
    3.  BLE roles
2.  Setup
    1.  Development environment
    2.  Testing environment
3.  BLE sensor implementation
    1.  First steps
    2.  Read from a sensor
    3.  BLE peripheral GAP
    4.  Add a sensor service
    5.  Add notifications
4.  Recap

# Concepts

## Bluetooth classic vs BLE

Bluetooth is a technology for wireless communications.  Although we talk about Bluetooth as if it were a single thing, Bluetooth Classic and Bluetooth Low Energy are mostly different beasts and also incompatible.  Bluetooth Classic has a higher transfer rate (up to 3Mb/s) than Bluetooth Low Energy (up to 2Mb/s), but with great transfer rate comes
great power consumption (as Spidey's uncle used to say).
, a mechanism created by Microsoft and implemented by some boards that emulates a storage device when connected to the USB port.  You can then drop a file into that storage device in a special format.  The file contains the firmware that you want to install with some metadata and redundancy and, after some basic verifications, it gets flashed to the microcontroller automatically.

In this case, we are going to flash the latest version of MicroPython to the RP2.  We press and hold down the BOOTSEL button while we plug the board to the USB, and we drop the latest firmware UF2 file into the USB mass storage device that appears and that is called RPI-RP2.  The firmware will be flashed and the board rebooted.
 a profile so you can have different extensions for different boards if needed. In this profile, you can also install the recommended Python extensions to help you with the python code.

Let's start by creating a new directory for our project and open VSCode there:
```sh
mkdir BLE-periph-RP2
cd BLE-periph-RP2
code .
```

Then, let's initialize the project so code completion works.  From the main menu, select `View` -> `Command Palette` (or Command + Shift + P) and find `MicroPico: Configure Project`.  This command will add a file to the project and various buttons to the bottom left of your editor that will allow you to upload the files to the board, execute them, and reset it, among other things.

You can find all of the code that is explained in the repository.  Feel free to make pull requests where you see they fit or ask questions.

## Testing environment

Since we are only going to develop the BLE peripheral, we will need some existing tool to act as the BLE central.  There are several free mobile apps available that will do that.  I am going to use "nRF Connect for Mobile" (Android or iOS), but there are others that can help too, like LightBlue (macOS/iOS or Android).

# BLE sensor implementation

## First steps

1.  MicroPython loads and executes code stored in two files, called `boot.py` and `main.py`, in that order. The first one is used to configure some board features, like network or peripherals, just once and only after (re)starting the board. It must only contain that to avoid booting problems. The `main.py` file gets loaded and executed by MicroPython right after `boot.py`, if it exists, and that contains the application code. Unless explicitly configured, `main.py` runs in a loop, but it can be stopped more easily. In our case, we don't need any prior configuration, so let's start with a `main.py` file.
2.  Let's start by blinking the builtin LED. So the first thing that we are going to need is a module that allows us to work with the different capabilities of the board. That module is named `machine` and we import it, just to have access to the pins:
    ```python
    from machine import Pin
    ```
3.  We then get an instance of the pin that is connected to the LED that we'll use to output voltage, switching it on or off:
    ```python
    led = Pin('LED', Pin.OUT)
    ```
4.  We create an infinite loop and turn on and off the LED with the methods of that name, or better yet, with the `toggle()` method.
    ```python
    while True:
      led.toggle()
    ```
5.  This is going to switch the led on and off so fast that we won't be able to see it, so let's introduce a delay, importing the `time` module:
    ```python
    import time
    
    while True:
      time.sleep_ms(500)
    ```
6.  Run the code using the `Run` button at the left bottom of VSCode and see the LED blinking. Yay!

## Read from a sensor

Our devices are going to be measuring the noise level from a microphone and sending it to the collecting station. However, our Raspberry Pi Pico doesn't have a microphone builtin, so we are going to start by using the temperature sensor that the RP2 has to get some measurements.

1.  First, we import the analog-to-digital-converting capabilities:
    ```python
    from machine import ADC
    ```
2.  The onboard sensor is on the fifth (index 4) ADC channel, so we get a variable pointing to it:
    ```python
    adc = ADC(4)
    ```
3.  In the main loop, read the voltage. It is a 16-bit unsigned integer, in the range 0V to 3.3V, that converts into degrees Celsius according to the specs of the sensor. Print the value:
    ```python
    temperature =  27.0 - ((adc.read_u16() * 3.3 / 65535) - 0.706) / 0.001721
    print("T: {}ºC".format(temperature))
    ```
4.  We run this new version of the code and the measurements should be updated every half a second.

## BLE peripheral GAP

We are going to start by advertising the device name and its characteristics.  That is done with the Generic Access Profile (GAP) for the peripheral role.  We could use the low level interface to Bluetooth provided by the `bluetooth` module or the higher level interface provided by `aioble`.  The latter is simpler and recommended in the MicroPython manual, but the documentation is a little bit lacking.  We are going to  start with this one and read its source code when in doubt.

1.  We will start by importing the `aioble` and `bluetooth`, i.e. the low level bluetooth (used here only for the UUIDs):
    ```python
    import aioble
    import bluetooth
    ```
2.  All devices must be able to identify themselves via the Device Information Service, identified with the UUID 0x180A. We start by creating this service:
    ```python
    # Constants for the device information service
    _SVC_DEVICE_INFO = bluetooth.UUID(0x180A)
    svc_dev_info = aioble.Service(_SVC_DEVICE_INFO)
    ```
3.  Then, we are going to add some read-only characteristics to that service, with initial values that won't change:
    ```python
    _CHAR_MANUFACTURER_NAME_STR = bluetooth.UUID(0x2A29)
    _CHAR_MODEL_NUMBER_STR = bluetooth.UUID(0x2A24)
    _CHAR_SERIAL_NUMBER_STR = bluetooth.UUID(0x2A25)
    _CHAR_FIRMWARE_REV_STR = bluetooth.UUID(0x2A26)
    _CHAR_HARDWARE_REV_STR = bluetooth.UUID(0x2A27)
    aioble.Characteristic(svc_dev_info, _CHAR_MANUFACTURER_NAME_STR, read=True, initial='Jorge')
    aioble.Characteristic(svc_dev_info, _CHAR_MODEL_NUMBER_STR, read=True, initial='J-0001')
    aioble.Characteristic(svc_dev_info, _CHAR_SERIAL_NUMBER_STR, read=True, initial='J-0001-0000')
    aioble.Characteristic(svc_dev_info, _CHAR_FIRMWARE_REV_STR, read=True, initial='0.0.1')
    aioble.Characteristic(svc_dev_info, _CHAR_HARDWARE_REV_STR, read=True, initial='0.0.1')
    ```
4.  Now that the service is created with the relevant characteristics, we register it:
    ```python
    aioble.register_services(svc_dev_info)
    ```
5.  We can now create an asynchronous task that will take care of handling the connections. By definition, our peripheral can only be connected to one central device. We enable the Generic Access Protocol (GAP), a.k.a General Access service, by starting to advertise the registered services and thus, we accept connections. We could disallow connections (`connect=False`) for connection-less devices, such as beacons. Device name and appearance are mandatory characteristics of GAP, so they are parameters of the `advertise()` method.
    ```python
    from micropython import const
    
    _ADVERTISING_INTERVAL_US = const(200_000)
    _APPEARANCE = const(0x0552) # Multi-sensor
    
    async def task_peripheral():
      """ Task to handle advertising and connections """
      while True:
        async with await aioble.advertise(
            _ADVERTISING_INTERVAL_US,
            name='RP2-SENSOR',
            appearance=_APPEARANCE,
            services=_DEVICE_INFO_SVC]
        ) as connection:
          print("Connected from ", connection.device)
          await connection.disconnected() # NOT connection.disconnect()
          print("Disconnect")
    ```
6.  It would be useful to know when this peripheral is connected so we can do what is needed. We create a global boolean variable and expose it to be changed in the task for the peripheral:
    ```python
    connected=False
    
    async def task_peripheral():
      """ Task to handle advertising and connections """
      global connected
      while True:
        connected = False
        async with await aioble.advertise(
            _ADVERTISING_INTERVAL_MS,
            appearance=_APPEARANCE,
            name='RP2-SENSOR',
            services=[_SVC_DEVICE_INFO]
        ) as connection:
          print("Connected from ", connection.device)
          connected = True
    ```
7.  We can provide visual feedback about the connection status in another task:
    ```python
    async def task_flash_led():
      """ Blink the on-board LED, faster if disconnected and slower if connected  """
      BLINK_DELAY_MS_FAST = const(100)
      BLINK_DELAY_MS_SLOW = const(500)
      while True:
        led.toggle()
        if connected:
          await asyncio.sleep_ms(BLINK_DELAY_MS_SLOW)
        else:
          await asyncio.sleep_ms(BLINK_DELAY_MS_FAST)
    ```
8.  Next, we import [`asyncio` to use it with the async/await mechanism:
    ```python
    import uasyncio as asyncio
    ```
9.  And move the sensor read into another task:
    ```python
    async def task_sensor():
      """ Task to handle sensor measures """
      while True:
        temperature =  27.0 - ((adc.read_u16() * 3.3 / 65535) - 0.706) / 0.001721
        print("T: {}°C".format(temperature))
        time.sleep_ms(_TEMP_MEASUREMENT_INTERVAL_MS)
    ```
10. We define a constant for the interval between temperature measurements:
    ```python
    _TEMP_MEASUREMENT_INTERVAL_MS = const(15_000)
    ```
11. And replace the delay with an asynchronous compatible implementation:
    ```python
    await asyncio.sleep_ms(_TEMP_MEASUREMENT_FREQUENCY)
    ```
12. We delete the import of the `time` module that we won't be needing anymore.
13. Finally, we create a main function where all the tasks are instantiated:
    ```python
    async def main():
      """ Create all the tasks """
      tasks = 
        asyncio.create_task(task_peripheral()),
        asyncio.create_task(task_flash_led()),
        asyncio.create_task(task_sensor()),
      ]
      asyncio.gather(*tasks)
    ```
14. And launch main when the program starts:
    ```python
    asyncio.run(main())
    ```
15. Wash, rinse, repeat. I mean, run it and try to connect to the device using one of the applications mentioned above. You should be able to find and read the hard-coded characteristics.

## Add a sensor service

1.  We define a new service, like what we did with the *device info* one. In this case, it is an Environmental Sensing Service (ESS) that exposes one or more characteristics for different types of environmental measurements.
    ```python
    # Constants for the Environmental Sensing Service
    _SVC_ENVIRONM_SENSING = bluetooth.UUID(0x181A)
    svc_env_sensing = aioble.Service(_SVC_ENVIRONM_SENSING)
    ```
2.  We also define a characteristic for… yes, you guessed it, a temperature measurement:
    ```python
    _CHAR_TEMP_MEASUREMENT = bluetooth.UUID(0x2A1C)
    temperature_char = aioble.Characteristic(svc_env_sensing, _CHAR_TEMP_MEASUREMENT, read=True)
    ```
3.  We then add the service to the one that we registered:
    ```python
    aioble.register_services(svc_dev_info, svc_env_sensing)
    ```
4.  And also to the services that get advertised:
    ```python
    services=[_SVC_DEVICE_INFO, _SVC_ENVIRONM_SENSING]
    ```
5.  The format in which the data must be written is specified in the "[GATT Specification Supplement" document. My advice is that before you select the characteristic that you are going to use, you check the data that is going to be contained there. For this characteristic, we need to encode the temperature encoded as a IEEE 11073-20601 memfloat32 :cool: :
    ```python
    def _encode_ieee11073(value, precision=2):
      """ Binary representation of float value as IEEE-11073:20601 32-bit FLOAT """
      return int(value * (10 ** precision)).to_bytes(3, 'little', True) + struct.pack('
## Add notifications

The "GATT Specification Supplement" document states that notifications should be implemented adding a "Client Characteristic Configuration" descriptor, where they get enabled and initiated.  Once the notifications are enabled, they should obey the trigger conditions set in the "ES Trigger Setting" descriptor.  If two or three (max allowed) trigger descriptors are defined for the same characteristic, then the "ES Configuration" descriptor must be present too to define if the triggers should be combined with OR or AND.  Also, to change the values of these descriptors, client binding --i.e. persistent pairing-- is required.

This is a lot of work for a proof of concept, so we are going to simplify it by notifying every time the sensor is read. Let me make myself clear, this is **not** the way it should be done.  We are cutting corners here, but my understanding at this point in the project is that we can postpone this part of the implementation because it does not affect the viability of our device.  We add a to-do to remind us later that we will need to do this, if we decide to go with Bluetooth sensors over MQTT.

1.  We change the characteristic declaration to enable notifications:
    ```python
    temperature_char = aioble.Characteristic(svc_env_sensing, _CHAR_TEMP_MEASUREMENT, read=True, notify=True)
    ```
2.  We add a descriptor, although we are going to ignore it for now:
    ```python
    _DESC_ES_TRIGGER_SETTING = bluetooth.UUID(0x290D)
    aioble.Descriptor(temperature_char, _DESC_ES_TRIGGER_SETTING, write=True, initial=struct.pack("
# Recap

In this article, I have covered some relevant Bluetooth Low Energy concepts and put them in practice by using them in writing the firmware of a Raspberry Pi Pico board.  In this firmware, I used the on-board LED, read from the on-board temperature sensor, and implemented a BLE peripheral that offered two services and a characteristic that depended on measured data and could push notifications.

We haven't connected a microphone to the board or read noise levels using it yet.  I have decided to postpone this until we have decided which mechanism will be used to send the data from the sensors to the collecting stations: BLE or MQTT.  If, for any reason, I have to switch boards while implementing the next steps, this time investment would be lost. So, it seems reasonable to move this part to later in our development effort.

In my next article, I will guide you through how we need to interact with Bluetooth from the command line and how Bluetooth can be used for our software using DBus.  The goal is to understand what we need to do in order to move from theory to practice using C++ later.

If you have questions or feedback, join me in the MongoDB Developer Community!

  [1]: https://images.contentstack.io/v3/assets/blt39790b633ee0d5a7/blt26289a1e0bd71397/6565d3e3ca38f02d5bd3045f/bluetooth.jpg
  [2]: https://images.contentstack.io/v3/assets/blt39790b633ee0d5a7/blte87a248b6f6e9663/6565da9004116d59842a0c77/RP2-bootsel.JPG