Showing posts with label Raspberry Pi. Show all posts
Showing posts with label Raspberry Pi. Show all posts

Friday, August 7, 2026

0

Turn Your Raspberry Pi Into a Smart AI Audio Classifier

Summary:

I am back with another Raspberry Pi automation project. This time I built a smart audio classifier utilizing YAMNet sound classification model and Adafruit digital MEMS microphones .The project can be useful for different applications including speech to text recognition, smart home security and industrial monitoring.

Hardware Setup:

 The following bill of materials is required:

https://cdn-learn.adafruit.com/assets/assets/000/090/544/original/sensors_pi_i2s_stereo_bb.png?1587498804

Wiring of I2S Mics to Raspberry Pi I2S interface 

Reference: https://learn.adafruit.com/adafruit-i2s-mems-microphone-breakout/raspberry-pi-wiring-test

 The following connections are made between the Raspberry Pi and the two digital mics:

  • Mics 3V to Pi 3.3V
  • Mics GND to Pi GND
  • Mics BCLK to BCM 18 (pin 12)
  • Mics DOUT to BCM 20 (pin 38)
  • Mics LRCL to BCM 19 (pin 35)
  • Left Mic SEL to Pi GND
  • Right Mic SEL to Pi 3.3V

Software Configuration and Debugging:

After assembling the setup I ran some checks to make sure the Pi can record audio and the audio quality is acceptable. To enable I2S interface on the Pi, the following line needs to be added to the Pi config.txt (or /boot/firmware/config.txt). A reboot is required for the new setting to take effect:

 dtoverlay=googlevoicehat-soundcard # enable i2s overlay

The above will assign specific GPIO pins for the I2S clocks (usually Pin 18 for BCLK, Pin 19 for LRCLK) and data lines (Pin 20 for Data In). To confirm a new audio recording device is registered in ALSA (Advanced Linux Sound Architecture) inspect the output of the following command:

 arecord -l

 It should print something like below. Note that the card number could be different:

**** List of CAPTURE Hardware Devices ****
card 2: sndrpigooglevoi [snd_rpi_googlevoicehat_soundcar], device 0: Google voiceHAT SoundCard HiFi voicehat-hifi-0 [Google voiceHAT SoundCard HiFi voicehat-hifi-0]
  Subdevices: 1/1
  Subdevice #0: subdevice #0 

To check the parameters associated with the audio device and its driver, I ran the following command:

 arecord -D hw:2,0 --dump-hw-params

The important output from this command tell the us the default configured sample rate, number of bits and channels



To record a stereo audio signal and save it into a wav file, I ran the following command:

arecord -D plughw:2 -c2 -r 48000 -f S32_LE -t wav -V stereo -v file_stereo.wav

Here is the exact breakdown of every argument in that line:

  • arecord The core ALSA command-line utility used for recording sound.

  • -D plughw:2 (Device) Tells ALSA which specific hardware device to listen to.

  • -c2 (Channels) Sets the number of recording channels to 2 (Stereo)

  • -r 48000 (Rate) Sets the sample rate to 48,000 Hz (48 kHz)

  • -f S32_LE (Format) Sets the digital audio format to Signed 32-bit Little Endian

  • -t wav (Type) Tells ALSA to package the recorded raw data into a standard .wav file container.

  • -V stereo (VU Meter) Displays a visual volume meter (a VU meter)

  • -v (Verbose) Turns on verbose mode.

  • file_stereo.wav The name of the file that will be saved to your Raspberry Pi's current directory when you stop the recording

In parallel, I attached a logic analyzer to inspect the I2S bit clock, left-right clock and data signals. From the capture below bit clock frequency is measured to be approx. 3.1 MHz which is close to microphone datasheet number. The word select signal frequency matches the desired sample rate of 48 kHz and the data line pulses are visible in both phases of the word select signal meaning both channels data are being transmitted from the left and right microphones respectively.

Capture of I2S (BLCK, LRCLK and DOUT) signals

Running Audio Classification with Python:

On software side, I used LiteRT Python package (successor to TensorFlow Lite) for running the YAMNet machine learning model on the Raspberry Pi. I built a minimal python script to load .wav audio files and run inference to verify that model can closely predict the audio events. You can find the python script and audio files I tested with in this Github repo.

I plotted the spectrogram of two audio files for testing to visualize what the front end of YAMNet mode will use for inferring the audio class. The model expects the audio frames to come in size of 15600 samples with sampling rate of 16 kHz. Only mono data is supported. 

Mel Spectrograms of two audio samples to be classified (left: miaow_16k.wav | right: speech_whistling2.wav)

Running the inference on the two samples yielded the following results. The Python script divided the audio files into required size for the model (i.e 0.96 sec) then inference ran individually on each frame. The class with highest confidence score was then qualified and printed along with the time it took to execute the inference in Python. After all audio frames are processed, the overall class is selected based on mean score of all detected classes. In summary, After listening the to the audio samples myself, the predicted classes are to a good extent accurate.

Wearables Audio Classification Setup:

Putting it all together, I wanted to see how good is the model with predicitng audio events based on  recordings from wearable devices (e.g headphones, hearing aids). It is of great potential that AI can augment human in the loop in manual testing where someone needs to have the golden ears to judge the quality of the audio. This is just a simple experiment however and the goal is not to have something very professional and application ready. 

To couple my headphones to the MEMS microphones, I created a simplified acoustic coupler using a 5ml plastic jar. The jar has two holes one against the microphone sound part with some space. The second is against the headphone audio output. With that the microphone should be able to capture the audio ouput of the headphones without much distraction from the surrounding acoustic environment. To play audio into my headset I used Bluetooth on the Raspberry Pi to connect and play the two audio samples I tested with the model in previous section. By comparing the model results with original vs. recorded samples we get a feeling on how well the overall solution work together (i.e. hardware recording + audio classification with machine learning)

Comparing the audio classes detected on recorded audio signal and comparing them to original audio yield close results overall. Optimizing the recording setup and post-processing audio after recording to remove noise artifacts and slight DC offset would improve the results.

Front and side views of headphones attached to DIY coupler with microphones also attached at the bottom
  
 
YAMNet predictions on original and recorded data for miaow_16k sample
 
YAMNet predictions on original and recorded data for speech_whistling2

Sunday, April 19, 2026

0

Running FreeRTOS on Raspberry Pi Pico 2

Summary:

In this blog post I wanted to document the steps to create a minimal firmware project to demonstrate running FreeRTOS kernel on the Raspberry Pico 2. I would use the offical FreeRTOS kernel repo on Github and add it as a dependency to be compiled insde a simple Pico Cmake project. The Raspberry Pi Pico 2 which hosts the RP2350 chip is listed as one of the community supported ports by the FreeRTOS project.

 


 

Installing Pico SDK and toolchain with VSCode:

To prepare the developement environment for writing a FreeRTOS project with VSCode IDE, the Raspberry Pi Pico extension need to be installed. It provides the latest SDK along with compiler and debugger toolchain. It also simplifies the process of creating cmkae project template and example projects from the Pico SDK.

RaspberryPi Pico VSCode Extension

Create C++ project and add FreeRTOS kernel :

The starting point is to create an empty C++ project using the Pico VScode extension. This will generate a template project structure with basic Cmake confgiuration files in addition to .c source file to host the code for the program to be run on the Pico.

Empty project configuration for Pico 2 board

 
Initial file strcutre for Pico 2 project

To verify that the toolchain  and sdk installation work we can run the "Compile Project" task from VScode Terminal > Run Task menu. If the command is susccesfull we should see a binary .elf image generated in the build folder. This image could then be flashed to a connected Pico board via the "Run Project" task.

Moving on .. To run FreeRTOS on the Pico 2 we need to add it as library to our existing project and include some Pico 2 specific configuration such that the kernel could run without compatibility issues. After that we can include FreeRTOS header files in our source code and write some example code to demonstarte the usage of RTOS tasks on the Pico.

First we need to add the FreeRTOS kernel repoistory to our existing folder as a git submodule. I have added it inside a new folder named "lib". We also need to fetch the submodules of FreeRTOS kernel repo which include the "ThirdParty" portable configrutions for the Pico board. These steps could be achieved with the following commands:

  • mkdir lib # create folder for adding the FreeRTOS kernel repo inside it.
  • git init # initialize a git repo for the current project
  • cd lib
  • git submodule add https://github.com/FreeRTOS/FreeRTOS-Kernel # add the FreeRTOS kernel
  • cd FreeRTOS-Kernel
  • git submodule update --init # get submodules also within FreeRTOS
  • git checkout --recurse-submodules V11.3.0 # checkout out a stable release version of the kernel.

 After adding the FreeRTOS kernel files to our project we need to add a Cmake file to signal the location of the kernel files for the target platform (i.e. Pico RP2350). we can copy this file over from the community supported port folder for the RP2350 as shown below:


In addition to the above .cmake file we need to include "FreeRTOSConfig.h" file to define the kernel configuration for the RP2350 hardware platform. I simply copied over a template config file from the raspberry pi pico-examples folder which also include the RP2350 specific options for the port to work. You can customize the config file later on according to your application need (e.g. enable static memory allocation instead of dynamic heap).

 


 After the above additions. The project file structure will look as follows:


Now we need to update the project CMakeLists.txt file to include reference to the FreeRTOS kernel to be compiled and linked with the Pico program executable. I have additionally enabled USB output so printf statement from the Pico program can be observed from serial terminal during runtime.

# Generated Cmake Pico project file

cmake_minimum_required(VERSION 3.13)

set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

# Initialise pico_sdk from installed location
# (note this can come from environment, CMake cache etc)

# == DO NOT EDIT THE FOLLOWING LINES for the Raspberry Pi Pico VS Code Extension to work ==
if(WIN32)
    set(USERHOME $ENV{USERPROFILE})
else()
    set(USERHOME $ENV{HOME})
endif()
set(sdkVersion 2.2.0)
set(toolchainVersion 14_2_Rel1)
set(picotoolVersion 2.2.0-a4)
set(picoVscode ${USERHOME}/.pico-sdk/cmake/pico-vscode.cmake)
if (EXISTS ${picoVscode})
    include(${picoVscode})
endif()
# ====================================================================================
set(PICO_BOARD pico2 CACHE STRING "Board type")

# Pull in Raspberry Pi Pico SDK (must be before project)
include(pico_sdk_import.cmake)

project(freertos-pico C CXX ASM)

# Initialise the Raspberry Pi Pico SDK
pico_sdk_init()

# Add executable. Default name is the project name, version 0.1

add_executable(freertos-pico freertos-pico.c )

# set RTOS kernel path and include its cmake file
SET(FREERTOS_KERNEL_PATH "${CMAKE_CURRENT_LIST_DIR}/lib/FreeRTOS-Kernel" CACHE STRING "Common Lib")
include(FreeRTOS_Kernel_import.cmake)


pico_set_program_name(freertos-pico "freertos-pico")
pico_set_program_version(freertos-pico "0.1")

# Enable USB output and disable UART
pico_enable_stdio_usb(freertos-pico 1)
pico_enable_stdio_uart(freertos-pico 0)

# Add the standard library to the build in addition to FreeRTOS
target_link_libraries(freertos-pico
        FreeRTOS-Kernel-Heap4 # FreeRTOS kernel and dynamic heap
        pico_stdlib)

# Add the standard include files to the build
target_include_directories(freertos-pico PRIVATE
        ${CMAKE_CURRENT_LIST_DIR}
)

pico_add_extra_outputs(freertos-pico)



After done with addition, configuration of FreeRTOS kernel and adaption of CMakeList.txt file, I create a simple FreeRTOS program which inludes one task to blink the onboard LED of the Pico 2 every 500 ms. print statements are added to the task to signal the toggiling process to serial terminal over USB. 

Thus, the "freertos-pico.c" looks as follows:

#include "pico/stdlib.h"
#include <stdio.h>
#include <FreeRTOS.h>
#include <task.h>

// Define the LED pin
const uint LED_PIN = 25;

void led_blink_task(void *pvParameters) {
    // 1. Initialize the GPIO
    gpio_init(LED_PIN);
    gpio_set_dir(LED_PIN, GPIO_OUT);

    while (true) {
        // 2. Toggle LED
        gpio_put(LED_PIN, 1);
        printf("LED ON !\n");
        // Delay for 500ms
        vTaskDelay(pdMS_TO_TICKS(500));

        gpio_put(LED_PIN, 0);
        printf("LED OFF !\n");
        // Delay for 500ms
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}


int main() {
    stdio_init_all();
    sleep_ms(2000);

    // Create the task
    // Name: "Blink", Stack size: 256 words, Priority: 1
    xTaskCreate(led_blink_task, "Blink_Task", 256, NULL, 1, NULL);

    // Start the scheduler
    vTaskStartScheduler();

    while (1); // Should never reach here
}


 

Compile and flash the program:

To compile the project we simply run the "Compile Project" task from VSCode Pico extension. If all runs smoothly with no errors we should see that couple of executable and binary files are generated inside the build folder.

 

To trnasfter the executable over to the Pico 2 board, we need to hold the "BOOTSEL" button before connecting the board to the host system via USB. Then we can either copy the ".uf2" binary generated above to the storage desk named "RP2350" or run the "Run Program" task from VSCode Pico extension. Both ways, the board will restart after the transfer process and we should see the green onboard blinking every 500 ms.

To inspect our program during runtime we can open a serial terminal to our Pico device and observe the trace messages printed from our FreeRTOS blink task.

Congratulations !. We just built a very simple Pico firmware based on the FreeRTOS kernel.

 

Friday, February 27, 2026

0

Exploring Bluetooth Lower Layers with Raspberry Pi Broadcom Chip

Background:

Whie looking for Bluetooth learning materials online targeting Raspberry Pi with BlueZ stack, I came across an interesting project on Github called: InternalBlue. It is a Bluetooth research framework targeting Broadcom chips (Cypress later and now Infenion). The framework allows interaction with Bluetooth low level layers below HCI (Hardware Controller Interface). This opens alot of possiblities to get a deeper understanding on how Bluetooth controller work under the hood and also perform security / performance analysis on off-the-shelf  Bluetooth devices.

Goal and Plan:

My goal was to check if the InternalBlue framework would actually work on the Raspberry Pi device as Pi models such as: Zero, 3, 3+ and  4 have built-in Broadcom (Cypress) chips by default. While the docuemntation says that Linux with BlueZ stack is supported, I could not really find a reference example verifying that it works on the Pi. There were some open Github issues discssing the topic and some suggested adaptions which were not verified. 

So my plan in summary was to take to take a deeper look into the Raspberry Pi use case to see if I can get it to work with the documented provided by InternalBlue developers. I picked a Raspberry Pi 3B and flashed it with a "very old" Raspbian "stretch" image to be at the same point of time when the project was active and increase my chances of success.

How InternalBlue Work: 

Before jumping into the tehnical steps of this experiemnt, I wanted to summarize the "theory of operation" of InternalBlue. The framework is based around the ability to patch the firmware running in RAM on certain Broadom (Cypress) chips. By doing this, addiotnal functions could be introduced in the firmware and existing implementation could be modified. Through reverse engineering of the Bluetooth firmware, developers were able to enable monitoring and injection of  LMP (Link Manager Protocol) packets. This is a very powerful research feature as LMP is located below HCI and gives insights on the Bluetooth controller behavior that could not be easily extracted wihtout the usage of expensive Bluetooth sniffers. The reasearchers utilized this to test couple of known Bluetooth bugs and security attacks.

InternalBlue Platform Overview (Credits: Jiska Classen, Secure Mobile Networking Lab)

Raspberry Pi Setup: 

I used an old Raspberry Pi 3B model to conduct my first experiment with InternalBlue. It has a built-in Broadcom (Now Cypress) Bluetooth/Wifi chip (Part number: BCM43438 / CYW43438). The Bluetooth controller supports both Bluetooth Classic and Bluetooth Low Energy.

  

On the software side, I flashed a pretty old linux image (Raspbian Stretch released April 2019) which has Linux kernel version 4.14.xx. It also has already BlueZ installed (version 5.43). The reason I went with such old image is becuase I was not sure if the stuff from InternalBlue still apply today to latest lernel/BlueZ and Broadcom Bluetooth firmware. Therfore, I travelled back in time to such old configuration when the InternalBlue project was under active development. 

According to kernel boot logs, The Bluetooth firmware (.hcd file) that gets loaded into the controller is: BCM43430A1 and is listed as one of the supported firmware files by the InternalBlue framework. I have not made any changes to this file and used the same version that got shipped with BlueZ.

 

Enable Broadcom Diagnostics Logs:

According to InternalBlue researchers, Broadcom chips have an undocumented diagnostic logging protocol that allows the forward of Bluetooth messages from the Baseband / Link Layer via HCI to the host. This was alredy a big important feature for me to test how Bluetooth pairing and connection takes place in these layers against different Bluetooth capable IOT devices. Anyway, after going through the documentation and couple of blog posts online I understood that there are two ways to enable this: The simplest one is to write the option flag provided by kernel/BlueZ: echo 1 >sys/kernel/debug/bluetooth/hci0/vendor_diag. Unfortanetly this failed on the Raspberry Pi with the built-in controller (exposed over UART). The vendor_diag option was missing and could not be found in the above directory.

After reading couple of Github issues on InternalBlue page, I understood that BlueZ has some "difficulties" identifying Broadcom chips when conncted via UART vs USB. I was not able to verify this and did not want to spend alot of time digging into BlueZ source code. 

The second option I came across is to write a vendor-specific HCI command to enable the diagnostic logs. The comand syntax provide by InternalBlue is this one: 

 

I got an error when trying to send this command via HCI tool (Unknow commnd). I believe that is because BlueZ does not support BCM_DIAG (0x07) messages in this case. I tried then to send the command directly via the Bluetooth scoket (see code snippet below). Here I got no errors but also did not see any response in BlueZ btmon tool I believe also for the same reason above. 

 

As I was hopeless that is not gonna work I came accross this Github issue which suggested that a kernel patch is needed such that the kernel can forward H4 BCM_DIAG messages and not filter it out. I decided to push forward and rebuild the linux kernel of the Raspberry Pi with the provided diff here.

Linux Kernel Rebuild:

This was the most ciritcal step of this experiment as I was not sure if my Pi would still boot after the kernel patch / rebuild and Bluetooth will still be functioning. The steps to apply the patch and rebuild the kernel were nevertheless straightforward. The kernel buid took around 2 hours on the Raspberry Pi 3B model. 

Here are the steps:

  • Clone the kernel source code 
git clone --depth=1 --branch rpi-4.14.y https://github.com/raspberrypi/linux
cd linux
  • Download and apply the diff via Git:
wget https://raw.githubusercontent.com/seemoo-lab/internalblue/master/linux/bias_linux-4.14.111.diff 
git apply -R path/to/your_file.diff
  • Configure and Build:
KERNEL=kernel7 
make bcm2709_defconfig 
make -j4 zImage modules dtbs
  • Install the kernel:
sudo make modules_install 
sudo cp arch/arm/boot/dts/*.dtb /boot/ 
sudo cp arch/arm/boot/dts/overlays/*.dtb* /boot/overlays/ 
sudo cp arch/arm/boot/dts/overlays/README /boot/overlays/ 
sudo cp arch/arm/boot/zImage /boot/kernel-ib.img
  • Switch to installed kernel and reboot:
sudo nano /boot/config.txt # 
Add this line at the end: kernel=kernel-ib.img
 

After rebooting the Pi, I was happy that everything is still working including Bluetooth :). In addition, it seems that Bluetooth diagnostic messages are now being forwarded by the kernel as shown in the logs. However,  HCI diagnostic messages (type 0x07) are still not visible in "btmon". Most likely BlueZ filters them out and we need to read the raw via Wireshark.

 

Running InternalBlue:

InternalBlue framework comes in the form of a Python command line tool. You need (Python >3.6) and I nedded to run the it with sudo otherwise sending commnds to the Bluetooth interface does not work.

I read the firmware build info from the chip by performing a memory dump at address 0x200400 and is listed under known firmware versions by the framework


Also reading the patchram region works !

Sending lmp commands gnerated warnings that it is not a supported feature on my setup. However, When checking the HCI btmon /dmesg logs. It seems that a Vendor HCI commad was sucessfully sent and also dmesg prints that a diagnostic packet was recieved. Since I am not able to see H4 diagnostic packets in btmon. I had to use the h4bcm_wireshark_dissector plugin used also inside InternalBlue framework to read the diagnostic packets as we will see later on .

 

Monitor LMP Packets with Wireshark :

The next step was to install Wireshark with the h4 dissctor plugin to be able to decode forwarded diagnostic packets. You need to use Wireshark 3.x version and build/install the h4bcm plugin from source. After installation I was able to start Wireshark capture session from InternalBlue command line.

To demonstrate the capture of diagnostic packets, I started a pairing session with my Bluetooth classic headphones. As you can see from the picture below, LMP packets indeed appear along with the standard HCI traffic. We can clearly see the SDP paring process steps on HCI and Link layers !


 Next, I tried to sendlmp command op code 1 from InternalBlue commandline with offset zero. This op code corresponds to name request on the link layer and our remote device should then reply with "LMP_name_res" containg the first sgement of the name with offset zero as shown below:


Conclusion:

In summary, I managed to reproduce the InternalBlue working concept on Raspberry Pi as there were no published attempts of doing so. We survived a kernel rebuild and managed to monitor and inject LMP stuff. In the future, I might try to test this on later versions of Raspberry Pi and BlueZ. Until then happy Bluetooth experimenting with this soluion ! ::)

Thursday, December 4, 2025

0

Automate Bluetooth IoT Product Testing using Raspberry Pi and BlueZ

Introduction:

 

With the rapid spread of innovative IoT devices such as sensors, werables and wireless headphones, a lot of effort is spent on Bluetooth connectivity in the development process to ensure newly available features from the Bluetooth standard are quickly shipped to the market to create a competitive advantage. This in turns requires significant amount of verification and testing to ensure that the delivered Bluetooth solution is not buggy and does not break compatibility with existing Bluetooth devices in the field (e.g. existing Android / iOS devices).

With the introduction of Bluetooth Low Energy, LE Audio and Auracast in conjunction with existing Bluetooth Classic, the complexity and testing effort have increased. This means a firm would need to dedicate significant resources to verify their Bluetooth implementation. To reduce the cost of manual testing, a firm would likely invest in test automation to efficiently verify Bluetooth related workflows that users will perform in the field (e.g user trying to pair a smart-watch to smart-phone via Bluetooth). However, There are many challenges when it comes to Bluetooth test automation:

  • Bluetooth APIs are vendor and platform specific (e.g. If you want to test your product against Android / iOS devices, you need to maintain a lot of customization in your test framework).
  • In lack of standard high level Bluetooth APIs, you are forced in certain cases to use UI automation frameworks (e.g. Appium) to interactively automate a desired Bluetooth use case for a specific platform UI . This also adds unnecessary complexity and coupling to the testing process.
  • To have a sufficient testing coverage, you need to establish an extensive "hardware in the loop test setup" with different Bluetooth devices to test against your product. This renders a huge cost and maintenance effort for test development and execution.

Objective:

In this blog post, I share a low cost-effective solution to tackle some of the Bluetooth testing challenges listed above. The solution involves using BlueZ (Linux standard Bluetooth stack) running on Raspberry Pi (low cost micro-computer) to test Bluetooth functionality of a given product. Later I show how BlueZ D-Bus API can be utilized to communicate with BlueZ programmatically via Python and hence a Bluetooth testing application could be developed.

About BlueZ: 

BlueZ is the standard Bluetooth stack for Linux. It can handle both Classic Bluetooth (BR/EDR) and Bluetooth Low Energy (BLE), and is shipped by default with Raspbian OS (Debian-based Linux distribution) along with other Linux distributions.

The modular architecture of BlueZ provides the flexibility to control attached Bluetooth adapters regardless of the platform and runtime environment used. BlueZ communicates with a Bluetooth adapter  via the Host Controller Interface (HCI) which is a standard interface that is defined in the Bluetooth Core Specification and is the basis of all Bluetooth protocols stacks.

BlueZ community maintains a well compatible stack with support for different Bluetooth controllers from various vendors. This facilitates the process of integrating different Bluetooth chips and control them from a standard interface without having to worry about the low level implementation of different vendors.

BlueZ management dameon process "bluetoothd" runs in user space and exposes high level Bluetooth functionality via a set of interactive command line tools: (bluetoothctl, btmon, l2ping , etc..) to perform Bluetooth related workflows (e.g. scan, pair, advertise, monitor and connect to Bluetooth devices).

When it comes to Bluetooth  automation with Linux, the command line tools listed above are not much of help. After some basic search on the web, I found out that BlueZ D-Bus API seems to be the recommended way to programmatically talk to the the Bluetooth dameon. The D-Bus API utilizes Linux inter-process communication bus (D-Bus). With this approach, you can establish a standard access to control and monitor Bluetooth tasks in Linux "asynchronously" without having to worry about the low level implementation behind these tasks.

Talking to BlueZ on the D-Bus:

To interact with the BlueZ D-Bus API, various libraries and wrappers already exist. If you are using Python in your software framework (e.g. for writing automated Bluetooth tests), The following libraries are of interest and will save you time trying to look at the D-Bus message specification:

  • Bleak (asynchronous, cross-platform Python API to communicate with BLE devices) 

However, if you are still interested in the details and want to build your custom D-Bus API for Bluetooth, The following resources are beneficial to get your started:

To demonstrate the usage of the D-Bus API, I created the examples below in Python

Bluetooth Discovery via Adapter Interface:

To control the Bluetooth adapter states and scan for nearby Bluetooth devices,  we need to talk to the "org.bluez.Adapter" interface. To test the code execution, we can open a parallel terminal session with "bluetoothctl" tool running which interactively displays all messages received / emitted by "bluetoothd"

import dbus
import time

# initialize a system d-bus object
bus = dbus.SystemBus()

# based on BlueZ d-bus API get the Adapter interface method and properties
# https://github.com/bluez/bluez/blob/master/doc/org.bluez.Adapter.rst

adapter_object = bus.get_object("org.bluez", "/org/bluez/hci0") # hci0 is the default Bluetooth hardware adapter
adapter_interface = dbus.Interface(adapter_object, "org.bluez.Adapter1")
adapter_props = dbus.Interface(adapter_object, dbus.PROPERTIES_IFACE)

# read all available properties of the adapter as d-bus dictionary
print(adapter_props.GetAll('org.bluez.Adapter1'))

# read individual properties by their name:
print(adapter_props.Get("org.bluez.Adapter1", 'PowerState'))
print(adapter_props.Get("org.bluez.Adapter1", 'Address'))

# set properties of the adapter
adapter_props.Set("org.bluez.Adapter1", 'Powered', False) # -> Disable Bluetooth adapter
adapter_props.Set("org.bluez.Adapter1", 'Powered', True) # -> Enable Bluetooth adapter

# start / stop discovery of Bluetooth devices
adapter_interface.StartDiscovery() -> Bluetooth adapter Discovering state changes to yes
time.sleep(30) # scan for 30 seconds
adapter_interface.StopDiscovery() -> Bluetooth adapter Discovering state changes to no

Pairing and Connection via Device Interface:

To pair with and connect to remote Bluetooth devices, we utilize the "org.bluez.Device" interface. The code snippet below is an example of interacting with the interface:

import dbus
import time

# initialize a system d-bus object
bus = dbus.SystemBus()

# get the DBus Object Manager to access currently available BlueZ device objects
manager = dbus.Interface(bus.get_object('org.bluez', '/'), 'org.freedesktop.DBus.ObjectManager')
managed_objects = manager.GetManagedObjects()

# look for BlueZ device interface objects and print found devices info
for obj in managed_objects.values():
device = obj.get('org.bluez.Device1', None)
if device:
print(f" Name: {device['Name']} , Address: {device['Address']}, Adapter: {device['Adapter']}, IsPaired: {bool(device['Paired'])}, IsConnected: {bool(device['Connected'])}")

# pair to a remote Bluetooth device using its address and Bluetooth adapter path:
default_adapter_path = '/org/bluez/hci0'
dev_relative_path = "dev_" + device['Address'].replace(":", "_")
remote_device_path = default_adapter_path + '/' + dev_relative_path
remote_device_object = bus.get_object('org.bluez', remote_device_path)
remote_device_methods = dbus.Interface(remote_device_object, 'org.bluez.Device1')
remote_device_props = dbus.Interface(remote_device_object, dbus.PROPERTIES_IFACE)

try:
remote_device_methods.Pair()
except Exception as e:
print(f"Excpetion occured during the pairing process: {str(e)}")

# wait for host / device to complete pairing process
time.sleep(30)

try:
remote_device_methods.Connect()
except Exception as e:
print(f"Excpetion occured during the connection process: {str(e)}")

Writing a Bluetooth test case with Python:

Now we have a basic understanding of how BlueZ D-Bus API can be accessed via Python, we can start creating our Bluetooth test functions. To make things easier, we can use an existing Python library like python-bluezero with high level functions to avoid working directly with D-bus objects as shown in previous examples and it also covers wide range of use cases for Bluetooth devices without adding much of complexity. This is a personal choice of course and you might use a different library that better suits your needs or build yours from scratch based on the BlueZ D-Bus API documentation.

In general, the test architecture diagram is depicted in the figure below, where we would be able to communicate with different Bluetooth adapters attached to the Raspberry Pi simply using Python and BlueZ D-Bus interface. Since the HCI interface between BlueZ and the Bluetooth hardware is governed by the Bluetooth standard, we do not need to write platform / vendor specific test functions. This greatly simplifies the process of testing our "Bluetooth Product". Of course, BlueZ is not free of bugs and limitations, However, it is still a free, open-source and capable tool to work with the Bluetooth technology.

 

In the code snippet below I created a simple test case using python-bluezero to scan for nearby Bluetooth devices, then pair and connect to a remote Bluetooth device with a given name. After connection is successful, I perform a series of disconnection / connection requests to verify that my Bluetooth device communicates as expected with the Bluetooth adapter on the Raspberry Pi. You can extend the test case to cover more interesting scenarios (e.g. interacting with GATT servers on a BLE device, stream audio from the Pi to a Bluetooth audio device and observe the status of Bluetooth audio profiles).
 
import logging
import pytest
from bluezero import adapter, central
from time import sleep

BT_NAME = "My_Bluetooth_Device" # Declare the name of your BT device under test

# create a pytest fixture to interact with the Bluetooth adapter on the system
@pytest.fixture(scope="session")
def bt_adapter() -> adapter.Adapter:
bt_dongles = adapter.list_adapters()
logging.info(f"Available BT adapters: {[str(bt_address) for bt_address in bt_dongles]}, -> selecting first adapter")
bt_dongle = adapter.Adapter(bt_dongles[0])
if not bt_dongle.powered:
logging.info("Powering on BT adapter")
bt_dongle.powered = True
return bt_dongle

def test_bt_pairing_and_connection(bt_adapter: adapter.Adapter) -> None:

logging.info(f"Discovering nearby BT devices ...")
target_bt_dev = None
bt_adapter.nearby_discovery(timeout=15) # scan for 15 seconds

# check target device is found
for bt_dev in central.Central.available(bt_adapter.address):
if bt_dev.name is not None and BT_NAME == bt_dev.name:
target_bt_dev = bt_dev

assert target_bt_dev, f"'{BT_NAME}' was not found"

logging.info(f"Pairing with: '{bt_dev.name} : {bt_dev.address}'...")
target_bt_dev.trusted = True
target_bt_dev.pair()
sleep(10) # allow sometime to resolve device info / services

# attempt connection after pairing / if device did not automatically connect
if target_bt_dev.connected == False:
target_bt_dev.connect()
sleep(10) # allow sometime for device to sync info

# check device is now connected
assert target_bt_dev.connected == True

# perfom disconnection / reconnection iterations
for _ in range(3):
target_bt_dev.disconnect()
sleep(10)
assert target_bt_dev.connected == False
target_bt_dev.connect()
sleep(10)
assert target_bt_dev.connected == True