Table of Contents
Introduction
This is the third and final post in my blog series exploring the potential of using AI LLMs, such as Google Gemini, to accelerate embedded code development.
Following my previous project developing the LSM303 6DoF driver, I wanted to now focus on a practical application using the wireless capability of the Pico 2 W development board.
So, in this blog, I'll describe how I developed a BLE-enabled 3D orientation sensing device (or digital spirit level) using AI LLM support. It will essentially be a BLE connectable device that will provide pitch, roll and yaw orientation data via a custom GATT service. I will also describe how I finished this project off with a “vibe coded” web app using HTML and JavaScript (with the Web Bluetooth API) to visualize the real-time data in 3D.
Overall, I found this new approach significantly reduced my development time compared to traditional software development methods. It's probably taken me longer to write up this blog than complete all this code for my project. Hopefully this blog demonstrates the how and the why.
The Raspberry Pi Pico BLE Library (C SDK)
The Raspberry Pi website offers two valuable PDF documents to get you started.
- Connecting to the Internet with Raspberry Pi Pico W-series. The title is somewhat misleading as this guide also covers connecting with BLE. There are three chapters on BLE with the first two being applicable for this project:
- Chapter 4. About Bluetooth
- Chapter 5. Working with Bluetooth and the C SDK
- Chapter 6. Working with Bluetooth in MicroPython
- Raspberry Pi Pico-series C/C++ SDK. This reference guide details the C/C++ libraries and tools for the Pico series. Two sections are particularly relevant:
- Section 2.3.9. This provides details on where to find documentation about the BTstack library (pico_btstack_ble), which is a Bluetooth stack written in C99 by BlueKitchen GmbH. This section also mentions how to enable Bluetooth Classic.
- Section 4.4. This provides info on the Pico Networking Libraries including pico_cyw43_driver and pico_cyw43_arch.
Also very helpful, especially for learning, are two online projects by V. Hunter Adams, an Electrical Engineering lecturer at Cornell University, which frame the Bluetooth stack and code architecture and explain BLE GATT client and server implementation:
- https://vanhunteradams.com/Pico/BLE/GATT_Client.html
- https://vanhunteradams.com/Pico/BLE/GATT_Server.html
Finally, let's not forget the Raspberry Pi Pico-examples repository on GitHub, which provides a comprehensive set of examples illustrating how the BLE library (BTstack) can be integrated in various applications.
For Bluetooth examples, you can either open the pico_w folder or simply scroll down to find the "Pico Bluetooth" section in the README, where numerous projects are listed with descriptions.
https://github.com/raspberrypi/pico-examples?tab=readme-ov-file#pico-bluetooth
However, nothing beats learning by doing. If you're working with VS Code, the "Raspberry Pi Pico Project" extension offers a streamlined approach. To start a new project from an example, open the extension and choose the "New Project From Example" option. A selection page will appear, where you should first specify your board type to find relevant projects.
Pico (2) W BLE Standalone examples - Temperature Sensing
For BLE there are 3 relevant examples listed, namely picow_ble_temp_reader, picow_ble_temp_sensor and picow_ble_temp_sensor_with_wifi.
These examples are the same ones listed in the Github README, below all the other Pico Bluetooth examples. You’ll find these listed under the text “Some Standalone Bluetooth examples (without all the common example build infrastructure) are also available”.
Based on the descriptions given on Github, the example that most closely fitted my needs was the picow_ble_temp_sensor example. However, I soon discovered that no matter which standalone example you click on in Github it all points to the same page, namely:
https://github.com/raspberrypi/pico-examples/tree/master/pico_w/bt/standalone
Unfortunately, this is the same with the "New Project From Example" option in VS Code. No matter which BLE example you select, the same project files are downloaded.
This is rather confusing, as I would have at least expected the configuration in the makefile (CMakeLists.txt) to correctly set up the project as either sensor or reader. Unfortunately, that is not the case.
So manual configuration of the CMakeList.txt file is required. That is, delete what you don’t need.
BLE Temperature Sensor example
In my case, I needed to use the Pico 2 W board to behave as a sensor (or peripheral device) and thus I deleted the Reader and Temp Sensor with wifi parts in the CMakeList.txt file.
Once I had the CMakeList.txt sorted, it was time to make sense of the various files that make up the project. Excluding CMakeList.txt, the key files making up a project are:
btstack_config.h |
Used to configure the BTstack library for your specific application. Used to enable features and set limits / allocate resources. |
server_common.c /server_common.h |
Functionality controlling BLE server behaviour. Handles advertising, connection & disconnection events, and then data reads/writes/updates etc. |
server.c |
The app's main project file. |
temp_sensor.gatt |
A BTstack formatted text file for defining your GATT services. When the project code is compiled, this file is automatically converted, in the background, to a more “machine readable” temp_sensor.h file. This header file is then stored in the “build >> generated” folder. |
Then not much else is required, other than compiling the code and flashing it onto the Pico 2 W. This can all be done inside VS Code.
Building a custom BLE application
Project Goal: More than just transmitting raw numbers
The goal for this project was simple: make the accelerometer data useful and visual. As such, a wireless (BLE) 3D digital spirit level seemed like a handy application. This prototype wireless tool would serve an excellent demonstration of how to use both the accelerometer and magnetometer’s capabilities in the real world.
With a basic driver already developed, providing raw values via the serial monitor (as detailed in my previous blog), my next challenge, particularly as a first-time endeavor, was to find a simple way to incorporate the BLE features of the Raspberry Pi Pico 2 W board.
As such I decided to utilize the temp_sensor project as my BLE code template. This would involve incorporating the accelerometer driver files into this project and then implementing the necessary BLE server customisation to handle the wireless data transfer and settings control.
It would also involve a bit of data smoothing and a bit of math (trigonometry) to advance from raw x, y, z values to actual angles such as pitch and roll, which would then define the object’s (i.e spirit level) spatial orientation:
- Roll (rotation around X-axis) can be calculated using atan2(Y_accel, Z_accel).
- Pitch (rotation around Y-axis) can be calculated using atan2(-X_accel, sqrt(Y_accel*Y_accel + Z_accel*Z_accel)).
- Yaw (rotation around the Z-Axis). This was a little more elaborate as it involved taking the raw magnetometer readings and then adjusting the calculated yaw based on roll and pitch.
Here, AI significantly streamlined the coding process. This specific functionality was completed, after a couple of prompt requests to Gemini. This essentially eliminated the need to consult datasheets and online resources to derive the correct formulae. While Google Gemini proved invaluable, the generated results still required verification, necessitating reliance on my own understanding of the underlying principles for validation. Fortunately, the conversion formulas and results were indeed correct.
Creating a custom BLE GATT service
My next step was to create my BLE service. Here I decided to create a custom GATT service with custom GATT characteristics for each of my calculated values, namely Roll, Pitch and Yaw (both raw and compensated). As the transmitted values had specific meaning, I added in some descriptors for each characteristic, namely 0x2901 to describe what each characteristic means and 0x904 to define the numerical format.
// GATT Profile for Pico 2 W 6DOF Sensor Demo Application // // Defines the Bluetooth Low Energy services and characteristics // for exposing sensor data (temperature, orientation) and sample rate. // Includes additional descriptors (0x2901 and 0x2904) for orientation sensor service // // This .gatt file then gets automatically converted to 6dof_sensor.h by the compile process. // The 6dof_sensor.h file can be found build >> generated >> pico2w_ble_6dof_sensor_gatt_header folder PRIMARY_SERVICE, GAP_SERVICE CHARACTERISTIC, GAP_DEVICE_NAME, READ, "picow_sense" PRIMARY_SERVICE, GATT_SERVICE CHARACTERISTIC, GATT_DATABASE_HASH, READ, PRIMARY_SERVICE, ORG_BLUETOOTH_SERVICE_ENVIRONMENTAL_SENSING // Characteristic Temperature - read only, dynamic, with notifications/indications CHARACTERISTIC, ORG_BLUETOOTH_CHARACTERISTIC_TEMPERATURE, READ | NOTIFY | INDICATE | DYNAMIC // New custom service for orientation PRIMARY_SERVICE, 19B10010-E8F2-537E-4F6C-D104768A1214 // Characteristic Sample Rate - read only, dynamic, write without response CHARACTERISTIC, 19B10012-E8F2-537E-4F6C-D104768A1214, READ | WRITE_WITHOUT_RESPONSE | DYNAMIC CHARACTERISTIC_USER_DESCRIPTION, READ | DYNAMIC // identifier: FORMAT-1, format: uint16_t, exponent:0, unit: frequency (hertz), name space: Bluetooth SIG, description: main , as defined in Assigned_Numbers.pdf CHARACTERISTIC_FORMAT, FORMAT-1, 06, 00, 2722, 01, 0106, // Characteristic Orientation Pitch - read only, dynamic, with notifications CHARACTERISTIC, 19B10013-E8F2-537E-4F6C-D104768A1214, READ | NOTIFY | DYNAMIC CHARACTERISTIC_USER_DESCRIPTION, READ | DYNAMIC // identifier: FORMAT-1, format: float32, exponent:0, unit: degree (plane angle), name space: Bluetooth SIG, description: main , as defined in Assigned_Numbers.pdf // IEEE 754 Single-Precision (often called "float32") CHARACTERISTIC_FORMAT, FORMAT-1, 14, 00, 2763, 01, 0106, // Characteristic Orientation Roll - read only, dynamic, with notifications CHARACTERISTIC, 19B10014-E8F2-537E-4F6C-D104768A1214, READ | NOTIFY | DYNAMIC CHARACTERISTIC_USER_DESCRIPTION, READ | DYNAMIC // identifier: FORMAT-1, format: float32, exponent:0, unit: degree (plane angle), name space: Bluetooth SIG, description: main , as defined in Assigned_Numbers.pdf // IEEE 754 Single-Precision (often called "float32") CHARACTERISTIC_FORMAT, FORMAT-1, 14, 00, 2763, 01, 0106, // Characteristic Heading Raw - read only, dynamic, with notifications CHARACTERISTIC, 19B10015-E8F2-537E-4F6C-D104768A1214, READ | NOTIFY | DYNAMIC CHARACTERISTIC_USER_DESCRIPTION, READ | DYNAMIC // identifier: FORMAT-1, format: float32, exponent:0, unit: degree (plane angle), name space: Bluetooth SIG, description: main , as defined in Assigned_Numbers.pdf // IEEE 754 Single-Precision (often called "float32") CHARACTERISTIC_FORMAT, FORMAT-1, 14, 00, 2763, 01, 0106, // Characteristic Heading Compensated - read only, dynamic, with notifications CHARACTERISTIC, 19B10016-E8F2-537E-4F6C-D104768A1214, READ | NOTIFY | DYNAMIC CHARACTERISTIC_USER_DESCRIPTION, READ | DYNAMIC // identifier: FORMAT-1, format: float32, exponent:0, unit: degree (plane angle), name space: Bluetooth SIG, description: main , as defined in Assigned_Numbers.pdf // IEEE 754 Single-Precision (often called "float32") CHARACTERISTIC_FORMAT, FORMAT-1, 14, 00, 2763, 01, 0106,
This is where Google Gemini struggled. It was not able to correctly define the format required for the descriptors inside this .gatt file. I had to discover how to do this manually and it was not easy, as I could not find any info in the official BTstack documentation on how to do this.
Nevertheless, Gemini had no problem giving me the correct characteristic format I needed to define my IEEE 754 Single-Precision (often called "float32") numerical values.
It was also not obvious where to add in the 0x2901 text descriptors. I tried to add text descriptors into the .gatt file and while it did not cause an error, these desciptors did not transfer across to the app when compiled. They simply got ignored. It required a forum query to get the solution, which revealed that this functionality is particular to the BTstack architecture as it's handled at runtime, via the att_read_callback function, when a user does a read request.
uint16_t att_read_callback(hci_con_handle_t connection_handle, uint16_t att_handle, uint16_t offset, uint8_t * buffer, uint16_t buffer_size) { UNUSED(connection_handle); switch (att_handle) { // enivonment service - temperature case ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_TEMPERATURE_01_VALUE_HANDLE: return att_read_callback_handle_blob((const uint8_t *)¤t_temp, sizeof(current_temp), offset, buffer, buffer_size); // orientation custom service - sample rate case ATT_CHARACTERISTIC_19B10012_E8F2_537E_4F6C_D104768A1214_01_VALUE_HANDLE: return att_read_callback_handle_blob((const uint8_t *)&sample_rate, sizeof(sample_rate), offset, buffer, buffer_size); // orientation custom service - pitch case ATT_CHARACTERISTIC_19B10013_E8F2_537E_4F6C_D104768A1214_01_VALUE_HANDLE: // Pitch return att_read_callback_handle_blob((const uint8_t *)&pitch_deg, sizeof(pitch_deg), offset, buffer, buffer_size); // orientation custom service - roll case ATT_CHARACTERISTIC_19B10014_E8F2_537E_4F6C_D104768A1214_01_VALUE_HANDLE: return att_read_callback_handle_blob((const uint8_t *)&roll_deg, sizeof(roll_deg), offset, buffer, buffer_size); // orientation custom service - heading raw case ATT_CHARACTERISTIC_19B10015_E8F2_537E_4F6C_D104768A1214_01_VALUE_HANDLE: return att_read_callback_handle_blob((const uint8_t *)&heading_deg_raw, sizeof(heading_deg_raw), offset, buffer, buffer_size); // orientation custom service - heading compensated case ATT_CHARACTERISTIC_19B10016_E8F2_537E_4F6C_D104768A1214_01_VALUE_HANDLE: return att_read_callback_handle_blob((const uint8_t *)&heading_deg_comp, sizeof(heading_deg_comp), offset, buffer, buffer_size); // description handles for each custom characteristic case ATT_CHARACTERISTIC_19B10012_E8F2_537E_4F6C_D104768A1214_01_USER_DESCRIPTION_HANDLE: return att_read_callback_handle_blob((const uint8_t *)DESCR_SAMPLE_RATE, strlen(DESCR_SAMPLE_RATE), offset, buffer, buffer_size); case ATT_CHARACTERISTIC_19B10013_E8F2_537E_4F6C_D104768A1214_01_USER_DESCRIPTION_HANDLE: return att_read_callback_handle_blob((const uint8_t *)DESCR_PITCH, strlen(DESCR_PITCH), offset, buffer, buffer_size); case ATT_CHARACTERISTIC_19B10014_E8F2_537E_4F6C_D104768A1214_01_USER_DESCRIPTION_HANDLE: return att_read_callback_handle_blob((const uint8_t *)DESCR_ROLL, strlen(DESCR_ROLL), offset, buffer, buffer_size); case ATT_CHARACTERISTIC_19B10015_E8F2_537E_4F6C_D104768A1214_01_USER_DESCRIPTION_HANDLE: return att_read_callback_handle_blob((const uint8_t *)DESCR_HEADING_RAW, strlen(DESCR_HEADING_RAW), offset, buffer, buffer_size); case ATT_CHARACTERISTIC_19B10016_E8F2_537E_4F6C_D104768A1214_01_USER_DESCRIPTION_HANDLE: return att_read_callback_handle_blob((const uint8_t *)DESCR_HEADING_COMP, strlen(DESCR_HEADING_COMP), offset, buffer, buffer_size); default: return 0; } }
Gemini was also not able to help me find a solution in this case, although it was fantastic with auto code completion. Once I had entered the line of code for "DESCR_SAMPLE_RATE" it was smart enough to work out that I wanted to repeat this for the rest of the descriptors within the switch case (all done through tab completion).
So overall, mixed success using AI here.
A handy step I discovered here is that once I had completed my .gatt file I could test it for completeness and accuracy by compiling the code and flashing it onto my Pico 2 W. Then I was able to use the nRFConnect app on my phone to scan and connect to the Pico 2 W and test. Here's a short video to demonstrate.
The final code development step for my custom BLE service was the event handling. Once again Gemini came to the rescue and delivered all the code changes for me.
In fact, Gemini was even able to resolve some bugs discovered through testing, such as this one shown in the video where I was getting duplicate event messages in the serial monitor (used for debugging).
There's too much code to show it all here but the Raspberry Pi Pico 2 W code can be found on my Github repository - I even got Gemini to do the README for me (talk about being lazy):
https://github.com/Gerriko/pico2w-ble-6dof-sensor
Vibe coding a custom web app to visualise the data
Now that I had the embedded code side sorted, I wanted to create some software to visualise the data as seeing numbers being streamed in real time on a phone or computer is pretty boring and somewhat meaningless to uninformed.
It then dawned on me that I could use the web Bluetooth API to connect my laptop to my Pico 2 W to receive the data, and then use some JavaScript to display the data. The only problem was that I had never developed any serious applications before using the web Bluetooth API as it was an experimental feature on a couple of browser platforms like the Chromium browser. So I did not want to spend hours learning the detail.
Enter Google Gemini Code Assist. With a couple of prompts it did it all for me (I reckon it was something like 96 to 98% of the code). I might have even pushed it a bit during the app development when it responded to one of my prompts with a "Okay, this is quite an update!". Usually it's responding with a "this request is a piece of cake" type of comment.
Here's a typical prompt I gave Gemini Code Assist to solve.
And here is the end result:
So, overall, not bad from Gemini Code Assist! I found it particularly useful in developing this webapp. In my opinion, I felt it does web dev much quicker than embedded code. It certainly had no problems with coming up with the functionality in JavaScript.
For those who want to code, it can be found in this GitHub repository. Here too, Gemini Code Assist created the README for me.
https://github.com/Gerriko/ble_scanner_WebApp_orientationSensor