Bluetooth connection between Arduino and iOS

Caio Piteli
Academy@EldoradoCPS
12 min readMay 13, 2024

Introduction

Hey!! If you’ve come here, I believe you’re looking for ways to connect and receive data from your Arduino device to your iOS device using bluetooth, right? (Here’s the GitHub link with a video illustrating what we’re going to build).

To do this, I’ve decided to divide the knowledge into two parts. The first will be concerned with hardware specifics, assembling the Bluetooth module and the code in Arduino to transmit the information. In the second part of this article, we’ll cover iOS development.

Bluetooth module

In this project, I chose to use Bluetooth to communicate between the Arduino and the IOS device because it is a form of communication that consumes less energy, something that is important when we are using the Arduino on a battery source.
With that in mind, the first question when we want to do our project is whether our Arduino already has bluetooth or not (for this, we can access its documentation). In my case, I’ll be using an ArduinoUno which doesn’t have bluetooth, so we need a module that provides this technology.

Which module to use?
iOS devices communicate using a technology called BLE (Bluetooth Low Energy) which has some differences compared to conventional Bluetooth, but that’s a topic for another article. So we need a module that can work with BLE, and we have a few options (pay attention before you buy yours), but I’m going to use the HC-08 module.

HC-08 bluetooth module picture
HC-08 module

Connecting HC-08 to Arduino

To better illustrate the assembly, I’ve created a diagram showing how it should be done.

Picture with the connection between Arduno UNO and HC-08 bluetooth module
Connection between ArduinoUNO and HC-08 bluetooth module

If in your project pins 2 and 3 are already being used, TX and RX can be connected to the available pins (in this case, we are using SoftwareSerial), however, when we get to the code, we must change which pins are being used.
If there are any problems with the image, I’ll write down how the connection is being made from the HC-08 to the Arduino:

  • VCC → 5V
  • GND → GND
  • TXD (transmission) → 2
  • RXD (reception) → 3

Coding time

The code that we will create to upload to our Arduino is intended to send the information to the bluetooth module (which will be connected to my iOS device) and it will send the data to my iOS device.

#include "SoftwareSerial.h"
SoftwareSerial bluetooth(3, 2); //3 = RX ; 2 = TX

Basically, in this part of the code we are using the SoftwareSerial library, which allows us to create additional serial ports on Arduino digital pins. This functionality is useful when we need serial communication, but we are already using the main serial port for other purposes, such as debugging the code via the serial monitor.

A SoftwareSerial instance is created with the name bluetooth. The parameters (3, 2) indicate that pin 3 of the Arduino will be used as RX (Receive) and pin 2 as TX (Transmit) (by default, the first number is RX and the second is TX).

⚠️If you have used different pins, change the numbers.⚠️

void setup() {
Serial.begin(9600);
bluetooth.begin(9600);
}

In our setup function (which is only executed the first time the script runs), we start serial communication between the Arduino and the computer via the standard serial port at a rate of 9600 bits per second (this is useful for debugging).

After this, we start the serial communication of the object created, called “bluetooth” at an equal rate of 9600 bits per second. This sets up communication between the Arduino and the Bluetooth module connected to pins 2 and 3.

void loop() {
bluetooth.print("Success!");
delay(2000);
bluetooth.print("Well done!");
delay(2000);
}

Now in our loop, the code that runs indefinitely, we are basically sending a string to the one responsible for communicating with the bluetooth module, each 2 seconds we send another string.

The idea in this code snippet is that you take the data that suits your project and send it through the communication created. In this case, we are only sending a string, but it is possible to send different types of data.

iOS Project

With what we’ve learned so far, our code and Arduino are ready to connect to any bluetooth device and send information, so now we need to create our SwiftUI iOS project.

XCode permissions

After creating our project in XCode, we need to add the use of bluetooth to our “info.plist”, so that the app will ask the user for permission to connect to other bluetooth devices.

Screenshot of info.plist
Screenshot of info.plist

We need to access the project file and then the “info” tab. When we access this screen, we need to place the mouse pointer over an existing item and click on the “+” to add the “Privacy — Bluetooth Peripheral Usage Description”.

This permission that we are going to add asks the user for permission to connect to Bluetooth devices and the “value” is the message that will be displayed to the user requesting the connection.

However, there is another permission called “Privacy — Bluetooth always usage Description”, which would ask the user to use Bluetooth constantly, even when the app is closed, a permission we also need for the project proposed here.

After adding both of the permissions, we should have two new lines in our “info” list:

Swift Coding

Now that we’ve got our Arduino ready and set up the necessary permissions to use Bluetooth, we can start our code.

In this sense, I’m only going to explain the part I’ve called “BluetoothController” in this article, while the view responsible for displaying the data and information present in this controller, you can access on GitHub, but basically it will display the information and data collected by the controller.

In this part of the medium article, I’ll put the section I want to explain and then I’ll divide it into smaller sections and explain each one. Every time you pass through the three dots, it indicates the beginning of the explanation of a new section.

This are the steps we are going to follow:

Steps followed on SwiftCoding: Discover peripherals -> Connect to peripheral -> Discover peripheral's services -> Discover service's characteristic -> Acces the characteristic needed -> Read the value wanted
Steps followed
import Foundation
import CoreBluetooth
class BluetoothController: NSObject, ObservableObject, CBPeripheralDelegate {

private var centralManager: CBCentralManager!

@Published var connectedPeripheral: CBPeripheral?
@Published var discoveredPeripherals = [CBPeripheral]()
@Published var isConnected = false
@Published var bluetoothStatus: BluetoothStatus = .off
@Published var valueReceived: String?

override init() {
super.init()
centralManager = CBCentralManager(delegate: self, queue: nil)
centralManagerDidUpdateState(centralManager)
}
}

The “BluetoothController” needs to be observable as I’m going to access some information in a view, it needs to be an NSObject as we’ll be using some ObjectiveC stuff and it needs to conform to CBPeripheralDelegate as it’s a protocol offered by CoreBluetooth which defines methods capable of handling events from peripherals (bluetooth devices that will be connected).

private var centralManager: CBCentralManager!

We have created a centralManager variable, of type CBCentralManager, since this is the type provided by CoreBluetooth that is responsible for managing and communicating with the bluetooth “peripherals”, specifically acting as a central in BLE communication.

@Published var connectedPeripheral: CBPeripheral?
@Published var discoveredPeripherals = [CBPeripheral]()
@Published var isConnected = false
@Published var bluetoothStatus: BluetoothStatus = .off
@Published var valueReceived: String?

This series of variables created is some information that I’m going to display in the view.

override init() {
super.init()
centralManager = CBCentralManager(delegate: self, queue: nil)
centralManagerDidUpdateState(centralManager)
}

Finally, we have our init, which basically manages to initialize our superclass, which in this case is the NSObject. Initializing everything provided by the NSObject before providing any additional setup for the BluetoothController.

centralManager = CBCentralManager(delegate: self, queue: nil) -> This line initializes an instance of CBCentralManager, which is the “central manager” for Bluetooth communication. It sets the “delegate” of the central manager to self, which means that the BluetoothController instance will receive returns related to Bluetooth events, such as state changes, peripheral discovery, etc. The queue parameter, set to nil, indicates that the central manager will use the main dispatch queue for returns from the delegate.

centralManagerDidUpdateState(centralManager) -> This line directly calls the centralManagerDidUpdateState method of the BluetoothController. This method is part of the CBCentralManagerDelegate protocol and is called when the Bluetooth state changes. By calling this method directly after the central manager has been initialized, it is guaranteed that the BluetoothController instance deals with the current Bluetooth state immediately after initialization.

extension BluetoothController: CBCentralManagerDelegate {

func centralManagerDidUpdateState(_ central: CBCentralManager) {
switch central.state {
case .poweredOn:
centralManager.scanForPeripherals(withServices: nil, options: nil)
bluetoothStatus = BluetoothStatus.on

case .poweredOff:
self.connectedPeripheral = nil
self.discoveredPeripherals = []
self.isConnected = false
self.valueReceived = nil
bluetoothStatus = BluetoothStatus.off

case .resetting:
// Wait for next state update and consider logging interruption of Bluetooth service
bluetoothStatus = BluetoothStatus.resetting

case .unauthorized:
// Alert user to enable Bluetooth permission in app Settings
bluetoothStatus = BluetoothStatus.unathorized

case .unsupported:
// Alert user their device does not support Bluetooth and app will not work as expected
bluetoothStatus = BluetoothStatus.unsupported

case .unknown:
// Wait for next state update
bluetoothStatus = BluetoothStatus.unknown

@unknown default:
print("---Default case---")
}
}

Within the “centralManagerDidUpdateState” method, depending on the current state of the central manager, different actions are taken:

  • .poweredOn, if Bluetooth is turned on, the central manager starts scanning for bluetooth devices.
  • .poweredOff, if Bluetooth is turned off, we clear all our references of devices that have already been discovered or connected, as we will have to discover everything again when we turn Bluetooth back on.

Within each state it is necessary to do the appropriate treatment, in this case I have only set the bluetooth status to the current state and have not dealt with these cases, but what each one means can be found in the documentation.

Essentially, this code monitors and responds to Bluetooth status changes to ensure that the application reacts appropriately to these changes.

Note: From here on, all the code must be inside this extension.

func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
if !peripheralAlreadyRegistered(peripheral: peripheral){
discoveredPeripherals.append(peripheral)
}
}

func peripheralAlreadyRegistered(peripheral: CBPeripheral) -> Bool{
return discoveredPeripherals.contains(peripheral)
}

Now, we’re going to enter a sequence of methods that are called automatically depending on the event that CentralManager detects, for example, previously when it was identified that the bluetooth was on, we started scanning for “peripherals” and as soon as we discovered one, we entered this first function “didDiscover”, which contains the actions to be done with the discovered peripheral.

In this case, we basically checked whether it had already been discovered or not, and if it hadn’t, we added it to our array of discovered peripherals (the array that will be displayed in the view, so that the user can choose which peripheral they want to connect).

func connect(peripheral: CBPeripheral) {
centralManager.connect(peripheral, options: nil)
}

func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
self.connectedPeripheral = peripheral
self.isConnected = true

peripheral.delegate = self
peripheral.discoverServices(nil)

}

Here we have two functions responsible for connecting to the peripheral selected in the View. When we click on the peripheral in our list of discovered devices, we call this “connect” function which asks our CentralManager to make the connection to the peripheral passed in as a parameter.

When the connection is successful, the LED on the Bluetooth module should stop flashing and stay on.

If the connection is successful, we automatically enter the CentralManager function “didConnect”, which in this case:

  • We store a reference to the connected device (connectedPeripheral)
  • Definition of the object itself as the “delegate” of the connected peripheral device. This allows the object to receive notifications about events that occur on the peripheral device.
  • We start searching for services (“features” that the Bluetooth device provides, it will be discussed further on this article) from our connected peripheral.
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
// Handle error
print("WARNING: Connection failed")
}

Another function that can be called when trying to connect to a peripheral is this one, when the connection is unsuccessful. In this case, I just printed a message on the console explaining that the connection was unsuccessful, but we must deal with this scenario.

func disconnect() {
guard let peripheral = connectedPeripheral else {
return
}
centralManager.cancelPeripheralConnection(peripheral)
}

func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
self.connectedPeripheral = nil
self.discoveredPeripherals = []
self.isConnected = false
self.valueReceived = nil

centralManager.scanForPeripherals(withServices: nil, options: nil)
}

Now, so that we don’t stay connected to a peripheral forever, we have the disconnect function which checks if we are connected to anything and if we are, we ask CentralManager to cancel the connection.

When this connection is canceled, I decided to clear all my references of devices found and connected, because what was previously connected is no longer connected. In addition, it may be that I connected my peripheral and walked 30 meters, losing other devices that I had already discovered, so I delete their reference so I won't try to connect to something that no longer exists.

After that, I start a new search for peripherals around me again.

Services

Basically, services are collections of characteristics that represent functionalities offered by the peripheral device. Each service can contain one or more features, which represent specific information provided by the peripheral device.

For example, a Bluetooth peripheral device may have a “Temperature Sensor” service which contains a feature to provide the current temperature measured by the sensor. In addition, it can have an “LED Control” service that contains characteristics for turning the LED on/off and setting its color.

func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
guard peripheral.services != nil else {
return
}
discoverCharacteristics(peripheral: peripheral)
}

Once we’ve dealt with the possibility of disconnecting from the device and the connection having failed, we go back to where we were, where we had called a function that will look for services and when these services are found, we enter this function which is now a method of our CBPeriperalDelegate, no longer of my CentralManager, so the events that happen on my peripheral must be dealt with through methods of the CBPeripeheralDelegate.

Our function that is called when services are discovered will basically check if that device has any services and if it does, we ask it to look for their characteristics.

func discoverCharacteristics(peripheral: CBPeripheral) {
guard let services = peripheral.services else {
return
}

for service in services {
peripheral.discoverCharacteristics(nil, 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: "FFE1") {
if characteristic.properties.contains(.notify) {
peripheral.setNotifyValue(true, for: characteristic)
}

self.connectedPeripheral?.readValue(for: characteristic)
break
}
}
}

The first function basically requests a search for the characteristics present in all the services found. When these characteristics are found, the CBPeripheralDelegate delegate identifies this event and calls the “didDiscoverCharacteristics” method.

In this method, we check that the list of characteristics is not null and from there, we go through all the characteristics found looking for the characteristic whose identifier is the string “FFE1”, as this is the characteristic that will provide us with the value we want, as well as having the notify property that will always warn us when the value changes.

After that, we read the value contained in this characteristic.

Caution

In the case of this project, where we are using the HC-08 module, the ID we are looking for is “FFE1”, however, it may be that if you are using another module, the ID is different.

To find out which feature you should look for, I recommend reading the datasheet of your module or Arduino. Alternatively, there is an APP called “LightBlue” which is able to connect to your device and provide some information about it, including some about the services provided by the device.

For those using the HC-08 module, I’ll leave the datasheet on gitHub.

LightBlue screenshot showing the information provided when connected to HC-08.
Informations provided by LightBlue when connected to HC-08
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
if let value = characteristic.value {
if let stringValue = String(data: value, encoding: .utf8) {
valueReceived = stringValue
}
}
}

Finally, after asking for the value to be read, if it changes, we enter the “didUpdateValueFor” method, which is responsible for transforming the received value into a string (since the purpose of this project is to display two strings that are sent by the Arduino).

However, in your project, you can make the transformation to the type of data you want to receive, and perhaps even make this transformation when you receive the data without having to wait for it to update its value. But in this case, the idea was to change the string every two seconds.

Conclusion

We have managed to build:

  • A class that can communicate with and receive data from bluetooth devices;
  • We learned how to use a bluetooth module in Arduino;
  • We have made it possible for Arduino and iOS to communicate via Bluetooth.

For our next steps, we need to access the information provided by the BluetoothController and use it in the desired view.

Visit my GitHub for the source code.
I hope I’ve helped!🫡

--

--