From Arduino to Android, light up Christmas with BLE

Building a smart Christmas tree with Bluetooth Low Energy

Leonardo Cavagnis
10 min readJan 15, 2024

--

This article refers to a workshop organized by GDG Milano. For the recorded session (only for Italian speakers, sorry 😅), you can find it here.

This guide explores the “magic” behind a Smart Christmas Tree designed for smartphone control. The main emphasis is on the integration of smart lighting technology, providing users with precise control over the illumination: from selecting specific colors to implementing various lighting effects. This article offers a practical roadmap for anyone interested in developing and implementing a Smart Christmas Tree or, more broadly, any smartphone-controlled project.

The big picture. Image by author.

How to do?

The implementation involves the development of an Android app that communicates via Bluetooth Low Energy with the Christmas tree. A prototype electronic system based on Arduino will be mounted on the tree, acting as the interface between the mobile app and the lighting system.
The Android app acts as a user-friendly platform, allowing the selection of colors and effects, while Bluetooth communication ensures a wireless and reliable connection between the smartphone and the tree.

Main technologies involved. Image by author.

The magic within the tree

The heart of our Christmas Tree is the Arduino Nano 33 BLE board.

The Arduino Nano 33 BLE is a compact, tiny, and powerful board. It is an evolution of the traditional Arduino Nano, but featuring a lot more powerful processor, sensors (accelerometer, gyroscope, and magnetometer), and connectivity.

Its primary tasks include providing control functionalities for the lighting system to the smartphone via BLE and managing the NeoPixel WS2812B LED strip: a programmable RGB LED strip, where each individual LED can be addressed and controlled, allowing for a wide range of colors and effects.

The entire system is powered by a 12VDC adapter since the USB power from the Arduino alone is insufficient to supply energy to the full prototype.

Main components behind the tree. Image by author.

Below, a detailed electrical schematic of the system.
The prototype is built on a breadboard, with a power supply module used to convert the incoming voltage level to the appropriate voltage for powering the board and LED strip.

Schematics. Image by author.

Bluetooth Low Energy 101

Bluetooth Low Energy (BLE) is a wireless communication technology designed for short-range communication. It is an extension of the classic Bluetooth and is specifically optimized for power efficiency.

BLE operates on a client-server architecture, involving two main roles: the central and the peripheral. The central device, such as a smartphone, communicates with peripheral devices, like smart objects.

BLE is supported by a wide range of devices and finds extensive applications in various fields, including IoT, fitness trackers, smart homes, and more.

Central-Peripheral. Image by author.

Services & Characteristics

In BLE, Services and Characteristics play fundamental roles in defining the functionalities and data exchange between devices.

  • A Service encapsulates a specific feature offered by a device.
  • A Characteristic represents a specific piece of data within that service.

Characteristics often contain information such as sensor data or control parameters, while services organize these characteristics into logical groups. Both characteristics and services are identified by a UUID (Universally Unique IDentifier), which is a standardized 128-bit value.

A characteristic can have one, none, or more of these properties:

  • Read: allows the central device to request and receive data from the peripheral.
  • Notify: enables automatic updates from the peripheral to the central device when the characteristic’s value changes.
  • Write: allows the central device to send data to the peripheral.

In our project, a service named Led is dedicated to managing the LED strip. Within this service, there are two characteristics:

  • The Effect characteristic, designed for both reading and writing, manages the application of various lighting effects through a one-byte enumeration (0=Off, 1=Steady, 2=Blink, 3=Alternate Blink).
  • The Color characteristic, also supporting both read and write operations, enables the control of RGB color values, encapsulated in three bytes.
Led Service. Image by author.

Scan & Advertising

Before establishing a connection between a central and a peripheral device, a prerequisite is the reciprocal act of searching and being found. Scanning represents the action performed by a central to find nearby peripherals, while advertising is a procedure where a peripheral actively announces its presence and shares information about its identity and features, making itself detectable.

Information that can be broadcast includes, for instance, the device name and the services it provides.

Scan & Advertising. Image by author.

Arduino: Let’s code!

In Arduino programming, a sketch is a program written in the Arduino IDE to control the behavior of the microcontroller, in this case, the Arduino board mounted on the Christmas tree.

A sketch consists of two fundamental parts:

  • The setup() function initializes variables, pin modes, and other configurations, running only once at the beginning of the program.
  • The loop() function contains the main code that runs repeatedly, executing the specified tasks.
Arduino sketch. Image by author.

In the context of this project, the Arduino sketch performs two activities:

  1. Advertising: to make the board discoverable by the Android app.
  2. LED strip control: the code interprets and translates BLE input data from the mobile app into actionable instructions for the LED strip, changing color and effect based on user preferences.

Let’s begin!

The setup() initiates with BLE.begin(), a function that initializes the Bluetooth Low Energy stack. BLE is an object associated with the <ArduinoBLE.h>class, a library designed to facilitate and enable BLE connectivity on Arduino boards.

Next, the BLE device name “GDG-Arduino” is assigned to the Arduino. This name, during advertising, helps the app find and connect to the tree.

#include <ArduinoBLE.h> // Arduino BLE Library

void setup () {
// BLE initialization
BLE.begin ();

// Set advertised local name
BLE.setLocalName("GDG-Arduino");

Exposing features

#include <ArduinoBLE.h>

BLEService ledService("103ce00-b8bc-4152-852-f096236d2833");
BLEByteCharacteristic ledEffectCharacteristic("103ce01-b8bc-4152-852-f096236d2833",
BLERead | BLEWrite);
BLECharacteristic ledColorCharacteristic("103ce02-b8bc-4152-852-f096236d2833",
BLERead | BLEWrite,
3);

void setup () {
// ...

// Add characteristics inside the LedService
ledService.addCharacteristic(ledEffectCharacteristic);
ledService.addCharacteristic(ledColorCharacteristic);

// Add LedService inside the BLE stack
BLE.addService(ledService);

// Set advertised service and start advertising
BLE.setAdvertisedService(ledService);
BLE.advertise();

//...
}

This segment of code exposes features essential for the control of the lighting system. The code begins by defining a service named ledService with a specific UUID. Within this service, two characteristics are established:

  • ledEffectCharacteristic, to manage the selection and control of lighting effects.
  • ledColorCharacteristic, to manage color selection for the LED strip.

The setup() function, also, configures and adds these characteristics to the ledService, integrating them into the BLE stack. Additionally, the code sets up the advertised service (BLE.setAdvertisedService(ledService)) and initiates the advertising process.

Managing App requests

The code within the loop() function controls the processing of incoming BLE write requests from the mobile app. It begins by listening for a BLE central device to establish a connection using BLE.central(). Once a device is connected, the code enters a loop to continuously monitor the incoming data.

Within this loop, the code checks if a write operation has been initiated on the ledEffectCharacteristic by using if(ledEffectCharacteristic.written()). If a write operation is detected, it retrieves the new value written to the characteristic and assigns it to the variable ledEffect = ledEffectCharacteristic.value(). After that, the applyEffect() function is called to implement the specified effect on the LED strip.

void loop() {
BLEDevice central = BLE.central(); // listen for BLE central to connect

if (central) { // if a central is connected to peripheral
while (central.connected ()) {
// Check ledEffect characteristic write
if (ledEffectCharacteristic.written ()) {
ledEffect = ledEffectCharacteristic.value();
}
// Check ledColor characteristic write
// ...

applyEffect (ledEffect) ;

Similarly, for the ledColorCharacteristic, the process remains the same, with the distinction that it reads 3 bytes instead of 1, enabling the customization of the LED strip color.

The applyEffect() takes a specified effect as an argument and uses the <Adafruit_NeoPixel.h> library to control easily the NeoPixel LED strip. For example, in the case of OFF_EFFECT, it turns off all LEDs, and in STEADY_EFFECT, it sets all LEDs to a predefined color.

void applyEffect (byte effect) {
switch(effect) {
case OFF_EFFECT:
ledStrip.clear();
ledStrip.show();
break;
case STEADY_EFFECT:
// Set all pixels to the specified color
for (int i = 0; i < LED_NUM; i++) {
ledStrip.setPixelColor(i, ledColor[0], ledColor[1], ledColor [2]);
}
ledStrip.show();
break;
//...

BLE on Android

In this part, it’s important to clarify that the article won’t cover the step-by-step creation of the mobile app from scratch. The focus is on describing fundamental concepts related to native Bluetooth APIs utilization within the Android OS.

Permissions

To use Bluetooth features in your Android application, you must declare three permissions in your AndroidManifest.xml file:

<manifest ... >
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
...
</manifest>
  1. BLUETOOTH to perform any Bluetooth communication, such as requesting a connection, accepting a connection, and transferring data.
  2. BLUETOOTH_ADMIN for more advanced operations, such as managing settings, conducting advanced scans, and handling connections.
  3. ACCESS_FINE_LOCATION permission because a scan can be used to gather information about the location of the user (for instance, BLE beacons in use at locations such as shops, museums, and more).
Needed permissions in Android. Image by author.

If your app targets Android 12 or later, declare the following additional permissions in the manifest file:

  • BLUETOOTH_SCAN permission, if your app searches for peripherals.
  • BLUETOOTH_ADVERTISE permission, if your app makes the current device discoverable by other devices.
  • BLUETOOTH_CONNECT permission if your app communicates with already-paired devices.

Bluetooth Manager

The BluetoothManager is a system service that provides an interface for managing and interacting with Bluetooth functionality on an Android device.

Through the BluetoothManager, we can obtain the BluetoothAdapter, which allows us to perform various operations such as discovering devices, checking the Bluetooth state, and establishing connections.

To obtain it, you can use the following code:

val _bleManager = context.getSystemService(BLUETOOTH_SERVICE) as BluetoothManager?
val _bleAdapter = _bleManager.adapter

Scan

Here is the code to initiate a Bluetooth device scan:

_bleAdapter.bluetoothLeScanner?.startScan(
mutableListOf(scanFilter), scanSettings, scanCallback
)
  1. .bluetoothLeScanner is the component designed for handling device scanning.
  2. .startScan(...) method is used to initiate a device scan.
  3. (mutableListOf(scanFilter), scanSettings, scanCallback) are parameters used to configure the device scan.

Below, a brief explanation of each parameter:

  • mutableListOf(scanFilter): A list of scan filters. A scan filter specifies the devices to look for based on specific criteria.

In this project, we address a filter criterion based on the service UUID of the ledService. This filter will match devices advertising the specified service.

ScanFilter.Builder()
.setServiceUuid( /* ledService UUID */ )
  • scanSettings: configures the scan settings, specifying parameters such as the scan mode.

The most used scan mode setting is the SCAN_MODE_LOW_POWER, which is optimized for minimal impact on the smartphone’s battery life.

ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
  • scanCallback: provides a method to be called when devices are found, or the scan fails.
val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
with(result.device) {
Log.i("ScanCallback", "Found BLE device! Name: ${name ?: "Unnamed"},
address: $address")
}
}
}

The onScanResult is a callback method provided by the ScanCallback class for handling the results of scanning. When the scanner identifies a device matching the ScanFilter, the method is triggered, providing information about the device in the ScanResult object.

To stop a device scan, use the function:

_bleAdapter.bluetoothLeScanner?.stopScan(scanCallback)

Connect

Once a device has been discovered, the next step is to establish a connection. Below is an example code snippet showing how to initiate a connection with a device:

_bleAdapter.getRemoteDevice("macAddress")?.let {
mBluetoothGatt = bluetoothDevice?.connectGatt(context, false, gattCallback)
}
  • _bleAdapter.getRemoteDevice("macAddress"): The code begins by obtaining an instance of a BluetoothDevice associated with the identified MAC address. The MAC address is used as a unique identifier for the device and you can acquire it during the scan phase.
  • bluetoothDevice?.connectGatt(context, false, gattCallback) initiates the connection with the device.

The connectGatt method takes as a parameter the gattCallback responsible for handling connection events.
The commonly used BluetoothGattCallback methods are:

  1. onConnectionStateChange: Notifies when the connection state of the device changes (connected or disconnected).
    To disconnect from a device, use the mBluetoothGatt?.disconnect() function.
  2. onServicesDiscovered: Indicates that the services on the peripherals have been discovered. After a successful connection, it is used to explore and interact with available services.
    To start services discovery use mBluetoothGatt?.discoverServices() function.
  3. onCharacteristicRead: Triggered when a characteristic value has been read.
  4. onCharacteristicWrite: Called after a write operation on a characteristic.
  5. onCharacteristicChanged: Notifies when the value of a monitored characteristic changes.

Read Characteristic

fun readLEDColor(): Boolean? {
val service =
mBluetoothGatt?.getService(/* ledService UUID */)
val characteristic =
service?.getCharacteristic(/* ledColor UUID */)

return mBluetoothGatt?.readCharacteristic(characteristic)
}

In the provided code snippet, the readLEDColor() function is responsible for initiating the read operation. It first obtains the ledService and ledColor characteristic using the respective UUIDs. The mBluetoothGatt?.readCharacteristic(characteristic) then triggers the read operation.

The response from the read operation is handled in the onCharacteristicRead callback method.

Write Characteristic

In the following code snippet, the setLEDEffect() function initiates the writing operation, specifically for setting the LED effect on the strip. It first identifies the ledService and ledEffect characteristic using the respective UUIDs. The payload variable is then created, containing the effect index converted to a byte. After that, the sendCommand() function is invoked with the characteristic and payload, performing the write operation.

fun setLEDEffect(effectIndex: Int): Boolean? {
val service =
mBluetoothGatt?.getService(/* ledService UUID */)
val characteristic =
service?.getCharacteristic(/* ledEffect UUID */)

val payload = byteArray0f(effectIndex.toByte())
return sendCommand(characteristic, payload)
}

The sendCommand() function manages the write operation. Within the function, the writeCharacteristic method is called on the mBluetoothGatt instance, specifying the characteristic, payload, and write type.

fun sendCommand(
characteristic: BluetoothGattCharacteristic?,
payload: ByteArray
): Boolean? {
characteristic?.let {
val result = mBluetoothGatt?.writeCharacteristic(
characteristic, payload, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
)
val res = when (result) {
BluetoothStatusCodes.SUCCESS -> true
else -> false
}
return res
}
return false
}

Code

--

--

Leonardo Cavagnis

Passionate Embedded Software Engineer, IOT Enthusiast and Open source addicted. Proudly FW Dev @ Arduino