Today I made a BLE RC car

Part 2 — Implementing small C++ BLEClient over Bluez

gal brandwine
Machines talk, we tech.
7 min readDec 28, 2022

--

In the previous post, I communicated with the ESP32’s BLE via the developers’ app nRF-connect . This way, I had a nice sandbox for playing around with my service characteristics. Also, it allowed me to check if the led on logic worked.

Eventually, I plan to have a client running on my laptop, controlling the car. For that, I need further learning.

Let’s briefly recap what I have until now

  1. An ESP32, with BLE initiated
  2. A GATT service is exposing my Service — UART_service along with its characteristics
  3. If led on is sent via the TX characteristic, the led turns on.

Now I’m about to replace the nRF-connect app with my own process, something like that:

  • my client is to connect the ESP32’s BLE and write/read data to/from the exposed Service.
  • Excitingly, there’s much more going on under the hood. In the laptop, unlike the ESP32 (a microcontroller that runs soft-realtime code), there’s an operating system running. And like many OS, every peripheral (Mouse, Keyboard, external GPU, WIFI, BLE, etc.) is managed by a driver. I run Linux; hence BLE communication is managed by BlueZ.
  • Communicating with my ESP32 requires first communicating with Bluez the driver; there are many ways to do so. But first, I played a bit with its CLI (Command line interface):
  1. In a terminal, I typed bluetoothctl

In the above image, I can see all the Service my laptop expose (they are presented as UUIDs)

2. Also, I can see all devices around me:

Keen eyes will notice the UART Service BLE devise.
My device :)

Connecting to a device is straightforward:

I’ve successfully connected to this device using bluetoothctl , also I can see all its exposed services.

Let’s explore deeper within bluetoothctlTyping — info 78:E3:6D:65:45:22

I can see three exposed services.

  • The first two are default and are always exposed unless explicitly configured otherwise.
  • The third Service Nordic UART Service is my own implemented Service.

NOTE:
My server is shown under this particular name because I use known UUIDs stored in the BT database. Anyone can register their UUID in this database, its costs money, though.

Exploring even deeper (in terminal)

# menu gatt                      //<------------ This exposes all GATTs supported CLI commands
[UART Service]# list-attributes //<------------ list all attributes of connected devices
Primary Service (Handle 0x0000)
/org/bluez/hci0/dev_78_E3_6D_65_45_22/service0001
00001801-0000-1000-8000-00805f9b34fb
Generic Attribute Profile
Characteristic (Handle 0x0000)
/org/bluez/hci0/dev_78_E3_6D_65_45_22/service0001/char0002
00002a05-0000-1000-8000-00805f9b34fb
Service Changed
Descriptor (Handle 0x0000)
/org/bluez/hci0/dev_78_E3_6D_65_45_22/service0001/char0002/desc0004
00002902-0000-1000-8000-00805f9b34fb
Client Characteristic Configuration
Primary Service (Handle 0x0000) <-------------------------- This my service (listed below are its Characteristic
/org/bluez/hci0/dev_78_E3_6D_65_45_22/service0028
6e400001-b5a3-f393-e0a9-e50e24dcca9e
Nordic UART Service
Characteristic (Handle 0x0000)
/org/bluez/hci0/dev_78_E3_6D_65_45_22/service0028/char0029
6e400003-b5a3-f393-e0a9-e50e24dcca9e
Nordic UART RX
Descriptor (Handle 0x0000)
/org/bluez/hci0/dev_78_E3_6D_65_45_22/service0028/char0029/desc002b
00002902-0000-1000-8000-00805f9b34fb
Client Characteristic Configuration
Characteristic (Handle 0x0000)
/org/bluez/hci0/dev_78_E3_6D_65_45_22/service0028/char002c
6e400002-b5a3-f393-e0a9-e50e24dcca9e
Nordic UART TX
[UART Service]#

I can read and write into those characteristics:

select-attribute 6e400002-b5a3-f393-e0a9-e50e24dcca9e <------- The Nordic UART TX
[UART Service:/service0028/char002c]# attribute-info <------- Check what can I do with this characteristics
Characteristic - Nordic UART TX
UUID: 6e400002-b5a3-f393-e0a9-e50e24dcca9e
Service: /org/bluez/hci0/dev_78_E3_6D_65_45_22/service0028
Flags: write
MTU: 0x0205

Typing write 0xff writes this value into my Service TX characteristic

And by monitoring the Serial connection with the ESP32, I can see the following loggings:

Device 0, had connected
*********
Received Value: �

*********

So I successfully communicated with my ESP32 through BLE via bluetoothctl

But hold your horses;
this is not the end of it. bluetoothctl is just a client communicating with Bluez the driver, something like this:

Now — things get interesting

Using SimpleBLE’s great examples, I could implement a simple C++ client that connects to theUART service exposed by the ESP32 over BLE.

This simple client sends hard-coded led on to the TX characteristics once a connection is made.

#include <simplebluez/Bluez.h>
#include <simplebluez/Exceptions.h>

#include <atomic>
#include <chrono>
#include <cstdlib>
#include <iomanip>
#include <iostream>
#include <thread>

SimpleBluez::Bluez bluez;

// Blah Blah Blah - Scroll down
std::atomic<bool> async_thread_active{true};
void async_thread_function() {
while (async_thread_active) {
bluez.run_async();
std::this_thread::sleep_for(std::chrono::microseconds(100));
}
}

// Blah Blah Blah - Scroll down
void millisecond_delay(int ms) {
for (int i = 0; i < ms; i++) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}

// Blah Blah Blah - Scroll down
void print_byte_array(SimpleBluez::ByteArray& bytes) {
for (auto byte : bytes) {
std::cout << std::hex << std::setfill('0') << (uint32_t)((uint8_t)byte) << " ";
break;
}
std::cout << std::endl;
}

std::vector<std::shared_ptr<SimpleBluez::Device>> peripherals;

int main(int argc, char* argv[]) {
int selection = -1;

bluez.init();
std::thread* async_thread = new std::thread(async_thread_function);

auto adapters = bluez.get_adapters();
std::cout << "Available adapters:" << std::endl;
for (int i = 0; i < adapters.size(); i++) {
std::cout << "[" << i << "] " << adapters[i]->identifier() << " [" << adapters[i]->address() << "]"
<< std::endl;
}

std::cout << "Please select an adapter to scan: ";
std::cin >> selection;
if (selection < 0 || selection >= adapters.size()) {
std::cout << "Invalid selection" << std::endl;
return 1;
}

auto adapter = adapters[selection];
std::cout << "Scanning " << adapter->identifier() << " [" << adapter->address() << "]" << std::endl;

SimpleBluez::Adapter::DiscoveryFilter filter;
filter.Transport = SimpleBluez::Adapter::DiscoveryFilter::TransportType::LE;
adapter->discovery_filter(filter);

adapter->set_on_device_updated([](std::shared_ptr<SimpleBluez::Device> device) {
if (std::find(peripherals.begin(), peripherals.end(), device) == peripherals.end()) {
std::cout << "Found device: " << device->name() << " [" << device->address() << "]" << std::endl;
peripherals.push_back(device);
}
});

adapter->discovery_start();
millisecond_delay(3000);
adapter->discovery_stop();

std::cout << "The following devices were found:" << std::endl;
int my_devices = 0;

// This is my ESP32's MAC I'm lookig for
const std::string my_device{"78:E3:6D:65:45:22"};

for (int i = 0; i < peripherals.size(); i++) {
if (peripherals[i]->address() == my_device) {
my_devices = i;
break;
}
std::cout << "[" << i << "] " << peripherals[i]->name() << " [" << peripherals[i]->address() << "]"
<< std::endl;
}

auto peripheral = peripherals[my_devices];
std::cout << "Connecting to " << peripheral->name() << " [" << peripheral->address() << "]" << std::endl;

for (int attempt = 0; attempt < 5; attempt++) {
try {
peripheral->connect();
millisecond_delay(1000);
} catch (SimpleDBus::Exception::SendFailed& e) {
millisecond_delay(100);
}
}

if (!peripheral->connected() || !peripheral->services_resolved()) {
std::cout << "Failed to connect to " << peripheral->name() << " [" << peripheral->address() << "]" << std::endl;
return 1;
}

// Store all services and characteristics in a vector.
std::vector<std::pair<std::shared_ptr<SimpleBluez::Service>, std::shared_ptr<SimpleBluez::Characteristic>>>
char_list;
for (auto service : peripheral->services()) {
for (auto characteristic : service->characteristics()) {
char_list.push_back(std::make_pair(service, characteristic));
}
}

std::cout << "The following services and characteristics were found:" << std::endl;
for (int i = 0; i < char_list.size(); i++) {
std::cout << "[" << i << "] " << char_list[i].first->uuid() << " " << char_list[i].second->uuid() << std::endl;
}

// This actualy turn on the LED on the ESP32!!!!!!!!!
// Wow! Cool!
// BLE is awesome
auto characteristic = char_list[2].second;
characteristic->write_command(SimpleBluez::ByteArray("led on"));

peripheral->disconnect();

// Sleep for an additional second before returning.
// If there are any unexpected events, this example will help debug them.
millisecond_delay(1000);

async_thread_active = false;
while (!async_thread->joinable()) {
millisecond_delay(10);
}
async_thread->join();
delete async_thread;

return 0;
}

For some, this chunk of code will be confusing and TMI.
For others quite obvious.

Regardless, this is what I have now.

I challenge you

[Linux] open a terminal, type bluetoothctl. Explore a bit.
For example — If you use BT headphones, investigate what Services they explore.

[MX Master 2S]# devices 
Device 73:5B:28:EA:42:9B 73-5B-28-EA-42-9B
Device 58:D6:E9:AE:AB:B1 58-D6-E9-AE-AB-B1
Device 6E:9A:E7:A5:8B:8F 6E-9A-E7-A5-8B-8F
Device 66:2C:1A:BF:92:6C 66-2C-1A-BF-92-6C
Device 76:28:02:F9:91:CD 76-28-02-F9-91-CD
Device DA:A4:58:46:AE:9F Halo_EP__42003070
Device 50:15:9B:74:65:C6 50-15-9B-74-65-C6
Device EB:53:9A:52:86:70 Halo_EP__10008666
Device 72:72:E8:73:BC:FA 72-72-E8-73-BC-FA
Device 64:90:C1:A3:CD:A3 64-90-C1-A3-CD-A3
Device 7C:C2:94:17:68:23 7C-C2-94-17-68-23
Device E1:05:3B:85:43:E5 Halo_EP__42002838
Device 54:EF:44:E2:C8:F2 Mi Smoke Detector
Device E4:7D:BD:D3:FA:89 [TV] Samsung 6 Series (49)
Device C5:8B:6F:04:98:78 MX Master 2S <----------------- My headphones
Device D8:37:3B:0A:E7:6D JBL Go 3
Device 4C:87:5D:0C:B5:FC LE-Bose QC35 II

You can even explore them deeper

[MX Master 2S]# info C5:8B:6F:04:98:78 <----------------- MAC of the headphones
Device C5:8B:6F:04:98:78 (random)
Name: MX Master 2S
Alias: MX Master 2S
Appearance: 0x03c2
Icon: input-mouse
Paired: yes
Trusted: yes
Blocked: no
Connected: yes
LegacyPairing: no
UUID: Generic Access Profile (00001800-0000-1000-8000-00805f9b34fb) // Default service
UUID: Generic Attribute Profile (00001801-0000-1000-8000-00805f9b34fb) // Default service
UUID: Device Information (0000180a-0000-1000-8000-00805f9b34fb) // These are stored in BT database
UUID: Battery Service (0000180f-0000-1000-8000-00805f9b34fb) // These are stored in BT database
UUID: Human Interface Device (00001812-0000-1000-8000-00805f9b34fb) // These are stored in BT database
UUID: Vendor specific (00010000-0000-1000-8000-011f2000046d) // This one is Bose proprietary (Not listed in DB)
Modalias: usb:v046DpB019d0006

Maybe try to read the Battery serivce ?
Maybe try to read the HMI?

Sum up

Now that I have my C++ client that can communicate with the ESP32 over BLE, I can peacefully continue working on my RC BLE Controller car :)

--

--