Skip to content

genkiinstruments/juce_bluetooth

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

30 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

JUCE Bluetooth LE module

JUCE module for interacting with Bluetooth LE devices on macOS and Windows.

Dependencies

The project depends on a few third-party libraries.

The CMake build script uses CPM to fetch these dependencies. If you use a different build system, you will have to make sure these libraries are available and linked properly as part of your appliation build step.

* JUCE will only be fetched if juce_bluetooth is loaded as a top-level project. If you're consuming it in your project, JUCE will surely already be available.

The project assumes a CMake-based environment.

Installing

In order to install the module in your system

cmake -B build -DCMAKE_INSTALL_PREFIX=/usr/local
sudo cmake --build build --target install

Then, in your project juce_bluetooth may be found using find_package. See the using_find_package example.

Quickstart

At the heart of your application you'll instantiate a BleAdapter to manage discovery and connections.

genki::BleAdapter adapter;

This module uses JUCE's ValueTrees to pass messages back-and-forth, so make sure to listen to changes on the adapter's state.

genki::ValueTreeListener listener{adapter.state};

listener.property_changed = [&](juce::ValueTree& vt, const juce::Identifier& id)
{
    if (vt.hasType(ID::BLUETOOTH_ADAPTER) && id == ID::status)
    {
        const auto is_powered_on = AdapterStatus((int) vt.getProperty(id)) == AdapterStatus::PoweredOn;

        fmt::print("{}\n", is_powered_on
                           ? "Adapter powered on, starting scan..."
                           : "Adapter powered off/disabled, stopping scan...");

        adapter.scan(is_powered_on);
    }
    else if (vt.hasType(ID::BLUETOOTH_DEVICE) && id == ID::last_seen)
    {
        if (vt.getProperty(ID::name).toString().isNotEmpty())
        {
            fmt::print("{} {} - rssi: {}\n",
                    vt.getProperty(ID::name).toString().toStdString(),
                    vt.getProperty(ID::address).toString().toStdString(),
                    (int) vt.getProperty(ID::rssi));
        }
    }
};

After discovery, you can connect to a BLE device

const BleDevice::Callbacks ble_callbacks{
    .valueChanged = [this](const juce::Uuid& uuid, gsl::span<const gsl::byte> data)
    {
        // Called when data is received on a characteristic via notifications or indications
    },
    .characteristicWritten = [this](const juce::Uuid& uuid, bool success)
    {
        // Called when a characteristic write has successfully been delivered to the peripheral
    },
};

listener.child_added = [&](juce::ValueTree&, juce::ValueTree& vt)
{
    if (vt.hasType(ID::BLUETOOTH_DEVICE))
    {
        const auto name = vt.getProperty(ID::name).toString();

        if (name.containsIgnoreCase(my_device_name))
        {
            device = adapter.connect(vt, ble_callbacks);
        }
    }
};

You'll receive ValueTree changes for all devices on the BleAdapter.state as well

After connecting to a device, you initiate service discovery

listener.property_changed = [&](juce::ValueTree& vt, const juce::Identifier& id)
{
    if (vt.hasType(ID::BLUETOOTH_ADAPTER) && id == ID::status)
    {
        ...
    }
    else if (vt.hasType(ID::BLUETOOTH_DEVICE))
    {
        if (id == ID::is_connected && vt.getProperty(id))
        {
            genki::message(vt, ID::DISCOVER_SERVICES);
        }
    }
};

which will emit a message with ID::SERVICES_DISCOVERED on the device node, allowing us to discover characteristics for a given service

listener.child_added = [&](juce::ValueTree&, juce::ValueTree& vt)
{
    if (vt.hasType(ID::BLUETOOTH_DEVICE))
    {
        ...
    }
    else if (vt.hasType(ID::SERVICES_DISCOVERED))
    {
        const auto dev = vt.getParent(); // dev should be the same node as device->state
        jassert(dev.hasType(ID::BLUETOOTH_DEVICE));

        if (const auto srv = dev.getChildWithProperty(ID::uuid, HeartRateServiceUuid.toDashedString()); srv.isValid())
            genki::message(srv, ID::DISCOVER_CHARACTERISTICS);
    }
};

and finally enable notifications on the characteristic

listener.child_added = [&](juce::ValueTree&, juce::ValueTree& vt)
{
    if (vt.hasType(ID::BLUETOOTH_DEVICE))
    {
        ...
    }
    else if (vt.hasType(ID::SERVICES_DISCOVERED))
    {
        ...
    }
    else if (vt.hasType(ID::CHARACTERISTIC))
    {
        if (juce::Uuid(vt.getProperty(ID::uuid).toString()) == HeartRateCharacteristicUuid)
            genki::message(vt, ID::ENABLE_NOTIFICATIONS);
    }
};