Basics of Design CC Blog Research & Design Hub

Smartphone App with ESP32

Written by Joseph Corleto

In this article, I discuss Bluetooth Low Energy (BLE) and demonstrate its basic implementation structure on the ESP32. I also cover how to create a Smartphone app on Thunkable which connects to the ESP32. Working with BLE can be challenging when using libraries and drivers from the manufacturer of the microcontroller (MCU). It can also be difficult to create a smartphone app to connect your BLE device, as it requires understanding mobile software development. I’ll show a straightforward method to get something up and running, and what’s possible with BLE development.

  • How do you implement BLE on an ESP32?
  • How can I create a smartphone app that connects to an ESP32?
  • How do you use Thunkable?
  • ESP32
  • BLE
  • Thunkable

It seems everything can be connected to a smartphone these days. Devices like refrigerators, watches, and cars are but a few items on that long list. As designers, engineers, and entrepreneurs, we need to keep up with technology’s constant, lightning-fast evolution. I found myself in this daunting position when I had to create a device whose settings could be configured using a smartphone over Bluetooth Low Energy (BLE).

I’m comfortable writing firmware for your run-of-the-mill electronics project, but I found learning the drivers needed to communicate over BLE to be a challenging endeavor. In my opinion, ESP-IDF, the framework that Espressif uses for its infamous ESP32 line of microcontrollers (MCUs), does a good job at accessing the BLE feature. But the other side of this project was the creation of a smartphone app that could communicate with this electronics device. I found that it was much easier to use a development platform called Thunkable. It touts itself as the easiest way to create smartphone apps with “no code.” Indeed, it was much easier than learning traditional mobile app development, and it was even possible to release my app through both App Store and Google Play.

What Is Bluetooth?

Bluetooth is a wireless technology created to replace cables for common devices such as computer peripherals (like a mouse or keyboard), speakers, and wearables. There are two types of Bluetooth devices: Bluetooth Classic and Bluetooth Low Energy. Bluetooth Classic is geared toward applications where data needs to be sent and received quickly. This comes at the cost of power consumption, and it does not lend itself well to battery-operated devices. Bluetooth Low Energy has lower data throughput, but it enables significantly longer lifespans for battery-operated devices—in some cases years.

Both flavors of Bluetooth operate on the same frequency spectrum of 2.4 GHz, but they’re incompatible with each other. That is to say that a BLE device cannot communicate with a Bluetooth Classic device, and vice versa. Smartphones can typically talk both Bluetooth Classic and BLE. I chose BLE not because my device was battery-operated, but because the exchange of data is low-bandwidth and at low duty cycles. Also, Thunkable only has a BLE block available to its developers, so learning the BLE portion of the ESP-IDF was the obvious choice. I later discovered BLE is the preferred choice for configuring a device’s settings, so it was a perfect fit for my project.

BLE Communication Steps

If you’ve used a BLE device before, you may have noticed that there is a very particular sequence that must be followed for two devices to connect to one another. Let’s imagine two such devices. We’ll call one a BLE central and the other a BLE peripheral. For this imaginary scenario, the central will be our PC, and the peripheral will be a computer mouse.

The devices are not initially connected, but are both interested in establishing a BLE connection. The PC, as a controller, scans the area for any kind of BLE device. The mouse advertises its data to any controllers scanning the area. After a few moments of scanning, the PC is able see that the mouse is in its vicinity. It can view information such as the mouse’s advertised name, as well as its proximity via a transmission power measurement. This process is called the discovery phase in BLE.

Next is the connecting phase. The PC decides that it would like to connect to the mouse, and it sends a connection request. If all goes well, the mouse sends back a connection response, and we’re now in the connected phase. This is where the most time is usually spent between the two devices. Three things can now occur: the PC can write data to the mouse, the PC can read data from the mouse, and the mouse can notify the PC that data is incoming.


Depending on who you talk to, there are either two or three blocks that make up BLE. Figure 1 shows a three-block architecture.

Figure 1
The architecture used in BLE devices can be grouped into three distinct blocks or levels.
Figure 1
The architecture used in BLE devices can be grouped into three distinct blocks or levels.

Let’s work our way down from the top. Applications, the first block, is how your device decides to handle data built on the Generic Access Profile (GAP) and the Generic Attribute Profile (GATT). They’re not really part of the BLE standard as they’re highly specific to your device. Say you have a smart light bulb which can have a brightness level written to it. How should you define this? One way is to have three different levels—low, medium, and high. You can further define that whenever you send the string “low” to the lightbulb, it changes to that level of brightness. This obviously also applies to the other two brightness levels. Or, you can send a value from one to 100, or one to 1,000, depending on how granular you want to get. 

Since the Applications layer is specific to the device, it’s up to the designer to define how the data is structured here. That is to say there’s no standard way to represent this data. So, is it appropriate to say it’s part of the BLE standard, since it doesn’t provide a structure for the data? Some programmers I’ve talked to don’t think so, and others do. To be on the safe side, it’s better to think about the Application layer as part of the overall BLE architecture, despite the freedom the designer has with the device’s data 

One level down we find the host block. This is where data is structured to answer questions like “how do I connect?” or “what data am I trying to convey?” This block also handles data packets that come from (and go to) lower levels in the BLE architecture. When developing a BLE device, you don’t need to know every detail in the host block. But two pieces to pay particular attention to are the GAP and the GATT. We will configure these and use them in our ESP32 API. The GAP and GATT will become clearer later in this article.

The last block is the controller, where the hardware lives. In this block, the physical layer is the actual radio used for communication. It modulates and demodulates the data. The link layer interfaces with the physical layer to provide the higher layers with an abstraction and a way for the host controller interface (HCI) layer to interact with the radio. The HCI layer is a protocol defined by Bluetooth specifications that permits the host block to easily talk to the controller block. Again, the details in this block are not crucial to understand BLE architecture. Just get a sense of what it does, and you’ll be able to use a BLE API easily.

ESP32 Bluetooth API

The ESP-IDF offers two different types of APIs to choose from: Bluedroid and NimBLE. Bluedroid can use both Bluetooth Classic and BLE, whereas NimBLE can only use BLE. For any projects that only require BLE, NimBLE is simpler to use—it has a smaller code footprint and a simpler API to get your project going. Unfortunately, NimBLE’s documentation on the Espressif website is not as substantial as Bluedroid’s. The good news is you can refer to the documentation written on Though the tutorials there won’t match one-to-one with ESP32 development, that site is still a better alternative to using Espressif documentation. Throughout this article, any BLE code is performed using the NimBLE API.

What Does GAP Do?

GAP gives you a framework to define how your BLE device will interact with other devices. The configuration of GAP parameters and features is not optional—every BLE device needs them for the discovery phase to work as intended. Through GAP, we can configure the mode and role of a BLE device, such as whether it should advertise or scan to establish a connection. We can set advertisement parameters like the RF power level, as well as how often to advertise and for how long. GAP allows us to set the device name, and it allows us to set connection parameters for accepting and initiating a connection. We can also configure security settings for our BLE device. The BLE device in this project will act as a peripheral, so we’ll configure advertising details, not scanning details. Remember, scanning is a role for a central device.

What Does GATT Do?

GATT is all about communication with the device’s specific data for its intended use. It’s easiest to explain how GATT works with an example. Suppose we have a BLE thermometer to which our smartphone is connected. In other words, these two devices are in the connected phase. The thermometer can report details about its battery and temperature, and can notify our smartphone when its environment reaches a certain temperature. In GATT terminology, we say that the thermometer has two different services which are differentiated by their Universally Unique Identifiers (UUIDs). Each service can have one or more characteristics. For example, the battery service can have characteristics such as the battery level in the format of a percentage. The battery level characteristic also has its own UUID, and has a read permission. Similarly, the temperature service also has its own characteristics that can tell us the current temperature the thermometer is measuring. The permissions here could be “read” and “notify.” Notify means that instead of our smartphone sending a request for information such as current battery level or temperature, the thermometer can let our smartphone know when it would like to send data—for example, when it hits 38⁰ Celsius. 

Since many types of service are shared by many devices, the Bluetooth specifications probably have a standard UUID for a given service. That is, we likely won’t need to define our own, but can instead use a standard service with standard characteristics. Nevertheless, if we can’t find a standard service that matches our device’s requirements, we can create a custom one and define our own UUID. In essence, GATT describes the features and types of data our BLE device will have. Once you understand the structure and use of UUIDs, services, and characteristics, using the NimBLE becomes an easier task. “Descriptors” configure how a characteristic works. Since my example does not have more than one way to define my device’s characteristics, we will not be using descriptors.

Trying It Out

Now we have just enough knowledge to try out a simple program with our ESP32-S3-DevKitC-1. As I mentioned earlier, the examples on Espressif are not as easy to follow when first starting out, so I tried to simplify the code as much as possible. Listing 1 is the drivers that are required to run our example.

Note that we included the NVS Flash. The NimBLE driver requires it. A driver for logging is not necessary, but I included it because I find it convenient for debugging. In Listing 2, we can see where our program begins executing. 

Once we reach the end, FreeRTOS takes over and prompts NimBLE to start churning away, as shown in Listing 3. Listing 4 is where NimBLE kicks in for our BLE communications. It will sync with the radio inside the ESP32 and run our callback named earlier. 

For this example, I wanted the ESP32 to advertise as soon as NimBLE started up. Earlier I explained that GAP lets us configure our advertising details—this is performed within the ble_app_advertise function. In Listing5, I configure some of what is possible with GAP, such as the device name, transmission power, and duration of advertising (in this case, forever).

At this point, the ESP32 will advertise and wait for a central device to scan it and initiate a connection. Once a device decides to connect, we jump into a special callback that lets us know what kind of GAP event took place. This is in Listing 6.

Here we can decide what to do when an event such as a connection or disconnection occurs. For example, if we disconnected from a device, I’d like the ESP32 to immediately start to advertise again. Once connected, our GATT portion takes over. We need to now create our services and characteristics, which is in Listing 7

The configuration structure is a little odd, but I promise you this as suggested in the API documents. Here, I define a custom characteristic with a custom service that is write-only. Note that the byte values in hex are completely arbitrary. You are free to set them to whatever you want, as they are not standard UUIDs. Also note that I define a callback function called incoming_message. When a central device writes data to that service and characteristic, that function will get called. This function is defined and implemented in Listing 8.

Listing 1 
This is the driver required to run our BLE example. Notice the words nimble, gatt, and gap appear on some lines.

#include <stdio.h>
#include “esp_log.h”
#include “nvs_flash.h”
#include “esp_nimble_hci.h”
#include “nimble/nimble_port.h”
#include “nimble/nimble_port_freertos.h”
#include “host/ble_hs.h”
#include “services/gap/ble_svc_gap.h”
#include “services/gatt/ble_svc_gatt.h”

uint8_t ble_address_type;
void ble_app_advertise(void);
Listing 2
This is the entry into our program. Here we initialize the NVS flash memory and set up BLE details, such as the device's name and the NimBLE API initialization.

void app_main(void)
    /* Initialize flash module */

    /* Initialize nimble internals and then init */

    // Set up name of the device
    ble_svc_gap_device_name_set(“MY ESP32”);    


    // This is where you jump to when your ble hw syncs
    ble_hs_cfg.sync_cb = ble_app_on_sync; 
Listing 3
“Host_task” is the first task that FreeRTOS will run within the ESP-IDF, and it’s where NimBLE starts to run. 

void host_task(void *param)
Listing 4  

This is where NimBLE syncs to the BLE radio.

void ble_app_on_sync(void)
    // Beacon requires a non-solvable address, do this below
    ble_hs_id_infer_auto(0, &ble_address_type);
Listing 5 
Our function that sets the advertising parameters is called when we’d like to start advertising so that a central device can request to connect and ultimately establish a connection.

/* Advertising settings and execution */
void ble_app_advertise(void)
    /* Setup struct for setting advertise fields */
    struct ble_hs_adv_fields fields;
    memset(&fields, 0, sizeof(fields));

    /* Set what fields you want */
    // Power levels
    fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_DISC_LTD;
    fields.tx_pwr_lvl_is_present = 1;
    fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO;

    // Name = (uint8_t *)ble_svc_gap_device_name();
    fields.name_len = strlen(ble_svc_gap_device_name());
    fields.name_is_complete = 1;

    // Now finalize the setting of fields

    /* Setup struct for setting advertise parameters */
    struct ble_gap_adv_params adv_params;
    memset(&adv_params, 0, sizeof(adv_params));

    // Connection type
    adv_params.conn_mode = BLE_GAP_CONN_MODE_UND;

    // Discovery mode
    adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN;

    /* Start advertising */
    ble_gap_adv_start(ble_address_type, NULL, BLE_HS_FOREVER, 
                        &adv_params, ble_gap_event, NULL);
Listing 6 

This callback for any GAP event is run when we have events such as a connection or disconnection.

/* Settings / callbacks for BLE GAP and advertisement */
static int ble_gap_event(struct ble_gap_event *event, void *arg)
            if(event->connect.status != 0)


    return 0;
Listing 7 

We define our services and characteristics in this structure. We setup the names of the callbacks, their UUIDs, and their types of permissions like read, write, and notify.

/* Define the UUIDs */
static const struct ble_gatt_svc_def gat_svcs[] =
        .type = BLE_GATT_SVC_TYPE_PRIMARY,
        .uuid = BLE_UUID128_DECLARE(0xff, 0xee, 0xdd, 0xcc, 
                                    0xbb, 0xaa, 0x99, 0x88, 
                                    0x77, 0x66, 0x55, 0x44,
                                    0x33, 0x22, 0x11, 0x00),
        .characteristics = (struct ble_gatt_chr_def[])
                .uuid = BLE_UUID128_DECLARE(0xfb, 0x34, 0x9b, 0x5f, 
                                            0x80, 0x00, 0x00, 0x80, 
                                            0x00, 0x10, 0x00, 0x00, 
                                            0xe1, 0xff, 0x00, 0x00),
                .flags = BLE_GATT_CHR_F_WRITE,
                .access_cb = incoming_message
Listing 8 
Here is our callback for our custom service and characteristic. It does nothing more than receive a message from the central device and print it to out to our log terminal.

/* Bluetooth (BLE) */
static int incoming_message(uint16_t conn_handle,
                            uint16_t attr_handle,
                            struct ble_gatt_access_ctxt *ctxt, 
                            void *arg)
    int data = *(ctxt->om->om_data);
    if(data == 49)
        ESP_LOGI(“BLE”, “Valid data!”);
    return 0;

In this callback I check if the first character from the central device happens to be a 1 or 49 (as defined in the ASCII table). If so, I will log a message to the terminal. Running this code will result in the ESP32 turning into a BLE peripheral to which you can connect and send data. Figure 2 is how it appears in a BLE scanning app called LightBlue. Figure 3 is what happens when I send a 1 from LightBlue to the ESP32.

Figure 2
LightBlue, the BLE Scanner app, scans our device MY ESP32, connects and inspects what data it exposes, and writes a 1 to our custom characteristic.
Figure 2
LightBlue, the BLE Scanner app, scans our device MY ESP32, connects and inspects what data it exposes, and writes a 1 to our custom characteristic.
Figure 3 
Sending a 1 to our ESP32 works as intended. If we were to send something else, like 2, nothing would print but the callback would still be executed.
Figure 3
Sending a 1 to our ESP32 works as intended. If we were to send something else, like 2, nothing would print but the callback would still be executed.

It looks like our firmware is ready to go. I am aware that this example is a bit complicated if you’re new to using NimBLE, but this is the most bare bones example I could muster.

Making an Android App

As I mentioned in the beginning of this article, creating a phone app requires knowledge of mobile software development. For the uninitiated, the prospect of getting into that can be daunting, and for my projects I couldn’t justify learning it in depth. This is how I came across Thunkable. If you understand how to program in any other language, Thunkable will be a breeze. In fact, it’s much easier than the ESP32 content I just covered. 

You first make an account on Thunkable’s website. The free option is all you need to run this demo. Next, run their sample project. Thunkable has a host of these, so we don’t need to do much to get an app going. These are easy to access through their YouTube video (Figure 4). You don’t need to watch their video, of course, but it provides a simple overview of how the project was created. In the video description, click on their project and it will open in your Thunkable account, ready to run. Once open, click “See Inside” in the top right of the editor to see the project details. After this, at the top right, the “Click to remix” button appears. This allows you to make edits by copying their sample project into your account’s workspace. To get to the code blocks, click on the tab “Blocks” on the top left and you will see something similar to Figure 5.

The first block can be thought of as a callback that occurs when a button to scan for BLE devices is clicked on the app. Once a list of devices is found, a label is updated to be rendered in the app. The second block provides the ability to connect to a device. In their example, they are connecting to something completely different than what we defined in our firmware. We’ll need to change this to what is in Figure 6. Once connected, another label will render to reflect the connection. Finally, in the third block, we can send off a string typed into our text box in the app. This is where we can send our 1 for testing to the ESP32. Notice that the UUID used is the same as what we declared in our firmware. This was done to make this project easier to follow.

Figure 4 
Thunkable's YouTube project 
video [1]
Figure 4
Thunkable’s YouTube project
video [1]
Figure 5 
This is Thunkable’s sample BLE project. Notice that they have their own UUID and device name.
Figure 5
This is Thunkable’s sample BLE project. Notice that they have their own UUID and device name.
Figure 6 
This is the same block code as Figure 5, but with the device changed to match our firmware example. We need to do this to properly connect through the app.
Figure 6
This is the same block code as Figure 5, but with the device changed to match our firmware example. We need to do this to properly connect through the app.

Thunkable has a browser simulator to run the app, but it won’t work properly with BLE code blocks. Instead, you will need to download the Thunkable Live app from the Google Play store [2]. You’ll be able to run the example from there. After running it, you will see the same result as that in Figure 3. I did not mention Apple devices, because Thunkable doesn’t have a method for easily testing this example on them. I believe that’s due to the particular way in which Apple software is created. With a paid account it’s possible to publish this app to the Google Play and App stores, but that is an incredibly long process and outside the scope of this article. Fortunately, that process is extensively detailed in Thunkable’s documentation.


As you can see, setting up BLE in a project can be quite complicated and involved. But don’t let that deter you from giving it a shot. MCUs such as the ESP32 have made it a lot easier to write programs in C within their ESP-IDF. Meanwhile, companies like Thunkable have made the app development process as painless as possible—you don’t have to understand the details of mobile software development. With some perseverance, I have been able to create two professional-looking apps for my own projects, and I believe you can too. If you would like to learn the ESP-IDF in more detail, I suggest for a primer course. 

[1] Thunkable’s project video:
[2] Thunkable Live app on Google Play:

Code and Supporting Files


Keep up-to-date with our FREE Weekly Newsletter!

Don't miss out on upcoming issues of Circuit Cellar.

Note: We’ve made the Dec 2022 issue of Circuit Cellar available as a free sample issue. In it, you’ll find a rich variety of the kinds of articles and information that exemplify a typical issue of the current magazine.

Would you like to write for Circuit Cellar? We are always accepting articles/posts from the technical community. Get in touch with us and let's discuss your ideas.

Sponsor this Article
+ posts

Joseph Corleto holds a Master’s Degree in Electrical Engineering. Aside from working as a full-time Electrical Engineer, he has a small business (Bit Bang Gaming LLC), which creates video game electronics hardware, and is actively pursuing the creation of a project-based video course using the ESP32. He describes himself as an Electrical Engineering handyman since he has skills in firmware, R&D, PCB design, PCB assembly, and automated testing. You may reach him anytime via his email:

Supporting Companies

Upcoming Events

Copyright © KCK Media Corp.
All Rights Reserved

Copyright © 2024 KCK Media Corp.

Smartphone App with ESP32

by Joseph Corleto time to read: 16 min