feeling-blue Documentation

build license

feeling-blue was created because of a lack of resources for the non-expert on how to connect to a bluetooth device using C++. The API was carefully designed to be easy-to-use, yet feature-rich so you can take advantage of your bluetooth peripherals without learning the nuisances of bluetoothLE. Okay, enough talking–let’s get started!

Installation

Download and build:

$ git clone https://github.com/seanngpack/feeling-blue-cpp/
$ cd feeling-blue-cpp
$ mkdir build
$ cd build
$ cmake ..
$ sudo make install

To use feeling-blue in your project, you gotta use cmake. Just call find_package and link the library. Here is what your CMakeLists.txt might look like:

cmake_minimum_required(VERSION 3.17)
project(yourProject)

set(CMAKE_CXX_STANDARD 17)

find_package(feeling-blue REQUIRED)

add_executable(yourProject main.cpp)
target_link_libraries(yourProject PUBLIC feeling-blue)

How to use and examples!

Read this before starting

When you create a new Central object, feeling-blue creates it on another thread. So you must keep your project’s main thread alive with some mechanism or else your program will exit immediately before you get to make bluetooth calls. If you want a quick and dirty way to demo the library, then just use a simple while loop to keep your main thread busy.

feeling-blue uses NSLog to display messages to your console. These messages may display out of order relative to your cout statements. But the code still runs in the order that you write it.

Connecting to your device

Connecting to your device only takes a couple steps!

Central initialization

To connect to your device, you must first initialize a Central. The central represents your computer and it manages Peripheral. Then you start up bluetooth.

std::unique_ptr<bluetooth::Central> central = std::make_unique<bluetooth::Central>();
central->start_bluetooth();

Search for peripheral

The peripheral represents the device you want to connect to. There is a limit to one peripheral per central. You can spawn more instances of central to connect to more devices.

To connect to a peripheral, you call the find_peripheral() method. If you know your device is advertising its localName, you can pass its name to find it:

std::shared_ptr<bluetooth::Peripheral> peripheral = central->find_peripheral("SwagScanner");

You can also connect to your peripheral if you know the 128-bit UUIDs of the services that it advertises.

std::vector<std:string> uuids = {service_uuid1, service_uuid2, etc}
std::shared_ptr<bluetooth::Peripheral> peripheral = central->find_peripheral(uuids);

Search for services

Peripherals can have many services. A service holds a collection of Characteristics, so a a serivce is a logical grouping of characteristics. A service may contain a service, but that is currently not officially supported, however…it may work.

To connect to a service, you call the find_service method and pass in its UUID as a std::string:

std::shared_ptr<bluetooth::Service> service = peripheral->find_service(service_uuid);

Search for characteristics

A Characteristic holds a value. feeling-blue stores this as a byte array. It is up to you to know how to convert this data into something human-readable, a string representation for instance. Future updates will include built-in functionality with the option to make it human-readable, but for now just byte the bullet.

Characteristic values can be read or written. A characteristic may also have notifications enabled meaning when the value is updated, it will notify your peripheral and send data to it.

To connect to a characteristic, call the find_characteristic method on the service that the characteristic belongs to and pass in the characteristic’s UUID as a std::string:

std::shared_ptr<bluetooth::Characteristic> rotate_char = service->find_characteristic(characteristic_uuid);

Code

main.cpp

int main() {

    std::string service_uuid = "5ffba521-2363-41da-92f5-46adc56b2d37";
    std::string char1_uuid = "5ffba522-2363-41da-92f5-46adc56b2d37";
    std::string char1_uuid = "5ffba523-2363-41da-92f5-46adc56b2d37";

    std::unique_ptr<bluetooth::Central> central = std::make_unique<bluetooth::Central>();
    central->start_bluetooth();
    std::shared_ptr<bluetooth::Peripheral> peripheral = central->find_peripheral("SwagScanner");
    std::shared_ptr<bluetooth::Service> service = peripheral->find_service(service_uuid);
    std::shared_ptr<bluetooth::Characteristic> characteristic1 = service->find_characteristic(char1_uuid);
    std::shared_ptr<bluetooth::Characteristic> characteristic2 = service->find_characteristic(char2_uuid);

    while (true) {
        ...blah blah
    }

    return 0;
}

Connecting to multiple devices

Okay, so you have multiple devices you want to connect to–no problem! As mentioned, a central can only manage one peripheral at a time. So to connect more, just instantiate more centrals and run through the connection and discovery steps again.

Below is some code creating two centrals and connecting them to their respective devices(I’ll leave connecting to services and characteristics to you):

main.cpp

int main() {

    std::unique_ptr<bluetooth::Central> smart_watch_central = std::make_unique<bluetooth::Central>();
    smart_watch_central->start_bluetooth();
    std::shared_ptr<bluetooth::Peripheral> smart_watch = smart_watch_central->find_peripheral("SmartWatch");
    ...find services and characteristics

    std::unique_ptr<bluetooth::Central> smart_clock_central = std::make_unique<bluetooth::Central>();
    smart_clock_central->start_bluetooth();
    std::shared_ptr<bluetooth::Peripheral> smart_clock = smart_clock_central->find_peripheral("SmartClock");
    ...find services and characteristics

    while (true) {
        ...blah blah
    }

    return 0;
}

Reading characteristic value

Let’s get some data! To read the value of your characteristic, call the read() method. This method blocks the calling thread until the data has been read from your characteristic and assigned to your variable. The read method is templated and supports multiple types described in the Supported Template Types.

std::vector<std::byte> data = characteristic->read<std::byte>();

If you want something more human-readable, and you know your device’s characteristic is represented in four bytes in little-endian order, you just just read your data as an integer:

int data = characteristic->read<int>();

By default, you should read in your data into std::vector<std::byte> unless you know for sure your device is outputting convertible types.

Writing to characteristic

There are two main options to write to your device. First we can write_without_response() which writes to your devices asynchronously and does not block your calling thread. If your write fails, you will not get a message telling you that it failed. This method is templated and supports multiple types described in the Supported Template Types.

std::vector<std::byte> data = {...};
characteristic->write_without_response<std::byte>(data);

And if you write with a response, then the method will block your calling thread and wait until your data has been successfully written to the device.

std::vector<std::byte> data = {...};
rotate_char->write_with_response<std::byte>(data);

Like the read() method, you can write to your device using different formats that will end up being converted to byte arrays.

Notifying characteristic

If your device and characteristic supports notifications, then let’s use it. First, just double check that your characteristic has notification support and that it’s enabled. So now when your device sends your computer notifications with a data payload you can capture that payload and write your own function to do something with it!

Non-member function event handler

Let’s write a callback event handler that takes in a std::vector<std::byte> and enable notifications, passing the function as a parameter.

IMPORTANT! All event handlers must follow this signature: void (std::vector<std::byte>)

void print_data(std::vector<std::byte> data) {
    for (auto const &b : data) {
        std::cout << (int)b << std::endl;
    }
}

characteristic->set_notify(print_data);

Member function event handler

member functions are a little trickier to write, but you just have to bind their class to std::function and add a placeholder parameter, then pass it like normal.

class A {
public:
    void print_data(std::vector<std::byte> data) {
        for (auto const &b : data) {
            std::cout << (int)b << std::endl;
        }
    }

    void set_notify(std::shared_ptr<bluetooth::Characteristic> c) {
        using namespace std::placeholders;
        std::function<void(std::vector<std::byte>)> binded_print_data = std::bind(&A::print_data, this, std::placeholders::_1);
        characteristic->notify(binded_print_data);
    }

private:
    std::shared_ptr<bluetooth::Characteristic> characteristic;
};

Passing member functions is really powerful because you can do things such as update an instance variable when notified.

If you’re passing the same function to multiple characteristic notifications, then just make sure your function contents are thread-safe, this applies to both member and non-member functions.

Public API

Below is a list of classes available to you. Click on the sidebar to find out more. All methods are synchronous unless otherwise noted!

class Central
class Characteristic
class Peripheral
class Service