Using Raspberry Pi Pico W to send data via Bluetooth to a SwiftUI app

Abijah KajabikaAbijah Kajabika
5 min read

The Raspberry Pi Pico W is a versatile microcontroller with built-in Bluetooth Low Energy (BLE) capabilities, making it ideal for IoT applications. The Raspberry Pi Pico SDK comes with Bluetooth and it would be great to use it to send some data to an iPhone app. In order to connect to iOS, we need to use Bluetooth Low Energy (BLE), that means we'll transfer data through the GATT protocol.

Prerequisites

  • A Raspberry Pi Pico W

  • A macOS computer with Xcode installed

  • Basic knowledge of C++ and Swift

  • An iOS device for testing

Setting Up the Raspberry Pi Pico W

First, let's set up the Raspberry Pi Pico W to act as a BLE peripheral, sending data to a SwiftUI app.

  1. Install the Pico SDK and Toolchain:

    Follow the official Raspberry Pi Pico C/C++ SDK documentation to set up your development environment. This includes installing the Pico SDK and necessary toolchains on your development machine. You can follow this awesome guide, but overall you should install the Raspberry Pi Pico SDK if not already and make sure you have Xcode installed on your computer (as we'll write an App for iOS).

  2. Create a BLE Peripheral Project:

    Start by creating a new C++ project for the Raspberry Pi Pico W. Here’s an example of how you can set up a simple BLE peripheral which sets up a BLE GATT server on the Pico W to send data.

     #include "pico/stdlib.h"
     #include "pico/cyw43_arch.h"
     #include "hardware/gpio.h"
     #include "btstack.h"
    
     static btstack_packet_callback_registration_t hci_event_callback_registration;
    
     static const uint16_t gatt_service_uuid = 0x181D;
     static const uint16_t gatt_characteristic_uuid = 0x2A19;
    
     static uint8_t adv_data[] = {
         0x02, 0x01, 0x06, // Flags: LE General Discoverable Mode, BR/EDR Not Supported
         0x0B, 0x09, 'P', 'i', 'c', 'o', 'W', '-', 'B', 'L', 'E', // Complete Local Name
         0x03, 0x03, 0x1D, 0x18  // Complete List of 16-bit Service Class UUIDs
     };
    
     static uint8_t value = 42; // Example data to send
    
     static int att_read_callback(hci_con_handle_t connection_handle, uint16_t attribute_handle,
                                  uint16_t offset, uint8_t * buffer, uint16_t buffer_size) {
         if (buffer) {
             buffer[0] = value;
         }
         return sizeof(value);
     }
    
     static int att_write_callback(hci_con_handle_t connection_handle, uint16_t attribute_handle,
                                   uint16_t transaction_mode, uint16_t offset, const uint8_t * buffer, uint16_t buffer_size) {
         if (buffer_size == 1) {
             value = buffer[0];
         }
         return 0;
     }
    
     void setup_gatt_server() {
         // GATT Service
         static const uint8_t gatt_service[] = {
             0x09, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x04, 0x00,
             0x00, 0x00, 0x00, 0x18, 0x1D, 0x00
         };
    
         // GATT Characteristic
         static const uint8_t gatt_characteristic[] = {
             0x08, 0x00, 0x00, 0x02, 0x00, 0x19, 0x2A, 0x02,
             0x00, 0x01, 0x00, 0x19, 0x2A, 0x08, 0x00
         };
    
         static uint16_t att_handle = 0x0001;
    
         att_server_init(att_read_callback, att_write_callback);
         att_db_util_add_service_uuid16(gatt_service_uuid);
         att_db_util_add_characteristic_uuid16(gatt_characteristic_uuid, ATT_PROPERTY_READ | ATT_PROPERTY_WRITE, ATT_SECURITY_NONE, &att_handle, sizeof(value), &value);
     }
    
     void packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) {
         if (packet_type == HCI_EVENT_PACKET && hci_event_packet_get_type(packet) == HCI_EVENT_LE_META) {
             if (hci_event_le_meta_get_subevent_code(packet) == HCI_SUBEVENT_LE_CONNECTION_COMPLETE) {
                 printf("Connected\n");
             }
         }
     }
    
     int main() {
         stdio_init_all();
         if (cyw43_arch_init()) {
             printf("Failed to initialize\n");
             return -1;
         }
    
         l2cap_init();
         sm_init();
         setup_gatt_server();
         hci_event_callback_registration.callback = &packet_handler;
         hci_add_event_handler(&hci_event_callback_registration);
         hci_power_control(HCI_POWER_ON);
    
         gap_advertisements_set_params(0x0800, 0x0800, 0x00, 0x00, 0x00, 0x00, 0x07);
         gap_advertisements_set_data(sizeof(adv_data), adv_data);
         gap_advertisements_enable(1);
    
         while (true) {
             cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 1);
             sleep_ms(1000);
             cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 0);
             sleep_ms(1000);
         }
    
         return 0;
     }
    

    This C++ code initializes the Pico W, sets up BLE advertising, and sends a simple "Hello from Pico W" message whenever a device connects.

  3. Build and Flash the Code:

    Compile the code using CMake and flash it to the Raspberry Pi Pico W. You can use the following commands in your terminal:

     mkdir build
     cd build
     cmake ..
     make
     sudo picotool load <your_program.uf2>
    

Building the SwiftUI App

Next, we’ll create a SwiftUI app to connect to the Raspberry Pi Pico W and receive data over BLE.

  1. Create a New SwiftUI Project:

    Open Xcode and create a new SwiftUI project named "PicoWGATT".

  2. Add Bluetooth Permissions:

    In the Info.plist file, add the following key to request Bluetooth permissions:

     <key>NSBluetoothAlwaysUsageDescription</key>
     <string>We need Bluetooth to connect to your Raspberry Pi Pico W</string>
    
  3. Set Up CoreBluetooth in SwiftUI:

    Now, implement the SwiftUI app to scan for the Raspberry Pi Pico W, connect to it, and display the received data.

     import SwiftUI
     import CoreBluetooth
    
     class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeripheralDelegate {
         var centralManager: CBCentralManager!
         var picoWPeripheral: CBPeripheral?
         @Published var receivedData: String = "No Data"
    
         override init() {
             super.init()
             centralManager = CBCentralManager(delegate: self, queue: nil)
         }
    
         func centralManagerDidUpdateState(_ central: CBCentralManager) {
             if central.state == .poweredOn {
                 centralManager.scanForPeripherals(withServices: [CBUUID(string: "181D")])
             }
         }
    
         func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
             picoWPeripheral = peripheral
             picoWPeripheral?.delegate = self
             centralManager.stopScan()
             centralManager.connect(peripheral, options: nil)
         }
    
         func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
             peripheral.discoverServices([CBUUID(string: "181D")])
         }
    
         func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
             guard let services = peripheral.services else { return }
             for service in services {
                 peripheral.discoverCharacteristics([CBUUID(string: "2A19")], for: service)
             }
         }
    
         func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
             guard let characteristics = service.characteristics else { return }
             for characteristic in characteristics {
                 if characteristic.uuid == CBUUID(string: "2A19") {
                     peripheral.readValue(for: characteristic)
                 }
             }
         }
    
         func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
             if let data = characteristic.value {
                 let receivedValue = data[0]  // Assuming data is a single byte
                 DispatchQueue.main.async {
                     self.receivedData = "Received: \(receivedValue)"
                 }
             }
         }
     }
    
     struct ContentView: View {
         @ObservedObject var bleManager = BLEManager()
    
         var body: some View {
             VStack {
                 Text("PicoW GATT Data")
                     .font(.title)
                     .padding()
                 Text(bleManager.receivedData)
                     .font(.largeTitle)
                     .padding()
             }
             .onAppear {
                 bleManager.centralManagerDidUpdateState(bleManager.centralManager)
             }
         }
     }
    
     @main
     struct PicoWGATTApp: App {
         var body: some Scene {
             WindowGroup {
                 ContentView()
             }
         }
     }
    

    This code uses CoreBluetooth to scan for BLE peripherals, connect to the Raspberry Pi Pico W, and display the received data in the SwiftUI app.

Running the App

  1. Run the C++ Program: Upload and run the C++ program on your Raspberry Pi Pico W. Just drag and drop the .uf2 file to the Pico and it'll reboot.

  2. Build and Run the SwiftUI App: Build and run the SwiftUI app on your iOS device. It should detect the Raspberry Pi Pico W, connect to it, and display the data it receives.

Conclusion

There you go, a quick view on how to use C++ on a Raspberry Pi Pico W to send data via Bluetooth Low Energy (BLE) to a SwiftUI app. This setup provides a simple way to communicate wirelessly between your Raspberry Pi Pico W and mobile devices, opening up a wide range of possibilities for IoT projects and real-time data communication.

Whether you're working on a sensor network, smart home automation, or educational projects, this combination of Raspberry Pi Pico W and SwiftUI allows for flexible and powerful development. If you code in React Native you can use the package react-native-ble-manager to achieve the same functionality.

7
Subscribe to my newsletter

Read articles from Abijah Kajabika directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Abijah Kajabika
Abijah Kajabika

Building stuff