Apple Watch Adventures

Communicate with BLE device Sensors at your wrist

There are many ways to read data from a device with the Apple Watch and probably the easiest is to buy a HomeKit device that has already a wifi protocol implemented and compatible with Apple Watch out of the box. Nevertheless there are many situations where one wants to communicate with a specific sensor, as it can be a thermometer, a moisture sensor, a pressure sensor, etc. and doesn’t find something already available on the market (or it is outrageous expensive). In this post I will show how to create the full code to read a pin from an Arduino Nano 33 BLE with the Apple Watch.

Arduino

To create a BLE device we use an Arduino Nano 33 BLE that is a great device for these kind of applications, but any BLE device should work just fine.

I am not going here in the details of the Bluetooth Low Energy protocol, but for this example is needed very little knowledge on how it works. The basic idea is that the Arduino will broadcast 3 things: the name of the device, the list of services registered on that device and the characteristics of each service. Imagine that to the same Arduino is connected a temperature sensor a 3-axis accelerometer. For each sensor there will be a service registered, so one service for the thermometer and one for the accelerometer, then the thermometer will have one characteristics (the reading of the temperature) while the accelerometer will have probably three characteristics (the acceleration on the three axis). In our example we are going to have only one service and one characteristics for that service because we will read only the PIN A0 and broadcast the value.

The code is the following:

#include <ArduinoBLE.h>

#define DEVICENAME "ArduinoSensor"
#define SERVICE "1101"
#define CHARACTERISTIC "2101"
#define SENSOR A0

BLEService sensorService(SERVICE);
BLEUnsignedCharCharacteristic serviceCharacteristic(CHARACTERISTIC, BLERead | BLENotify);

void setup() {  
  pinMode(LED_BUILTIN, OUTPUT);
  if (!BLE.begin()) 
  {
    while (1);
  }

  BLE.setLocalName(DEVICENAME);
  BLE.setAdvertisedService(sensorService);
  sensorService.addCharacteristic(serviceCharacteristic);
  BLE.addService(sensorService);

  BLE.advertise();
}

void loop() 
{
  BLEDevice central = BLE.central();

  if (central) 
  {
    digitalWrite(LED_BUILTIN, HIGH);

    while (central.connected()) {
      int sensor = analogRead(SENSOR);
      int sensorValue = map(sensor, 0, 1023, 0, 100);
      serviceCharacteristic.writeValue(sensorValue);
      delay(200);
    }
  }
  digitalWrite(LED_BUILTIN, LOW);
}

Apple Watch

The code for the Apple Watch is a bit more complicate for the nature of Swift and how the Apple Framework works, so who wants to use it should be familiar with Swift (and have a developer license from Apple).

The complexity anyway is not that big. The idea is to create a BLEManager class and define all the relevant methods to handle what happens with the device, specifically the methods should handle: didDiscover, didConnect, didDisconnectPeripheral, didDiscoverServices, didDiscoverCharacteristicsFor, didUpdateValueFor.

Everything can stay in one class in a Swift file as the following:

import Foundation
import CoreBluetooth

class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeripheralDelegate {

  	let sensorName = "ArduinoSensor"
    let serviceName = "1101"
    let characteristicName = "2101"
  
    var myCentral: CBCentralManager!

    @Published var isSwitchedOn = false
    @Published var status = ""
    var sensorValue: UInt8 = 0
    
    private var peripheral: CBPeripheral!

    override init() {
        super.init()

        myCentral = CBCentralManager(delegate: self, queue: nil)
        myCentral.delegate = self
    }

    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        if central.state == .poweredOn {
            isSwitchedOn = true
        }
        else {
            isSwitchedOn = false
        }
    }
    
    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        var peripheralName: String!
        if let name = advertisementData[CBAdvertisementDataLocalNameKey] as? String {
            peripheralName = name
        }
        else {
            peripheralName = "Unknown"
        }

        if peripheralName == sensorName {
            self.stopScanning()
            self.myCentral.connect(peripheral, options: nil)
            self.peripheral = peripheral
        }
    }
    
    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        peripheral.discoverServices(nil)
        peripheral.delegate = self
    }
    
    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral: CBPeripheral, error: Error?) {
        self.startScanning()
    }
    
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        if let services = peripheral.services {
            for service in services {
                if service.uuid == CBUUID(string: serviceName) {
                    peripheral.discoverCharacteristics(nil, for: service)
                }
            }
        }
    }
    
    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        if let charac = service.characteristics {
            for characteristic in charac {
                if characteristic.uuid == CBUUID(string: characteristicName) {
                    self.peripheral.readValue(for: characteristic)
                    if let data = characteristic.value {
                        self.sensorValue = data[0]
                        self.status = "Value: "+String(self.sensorValue)
                    }
                }
            }
        }
    }
    
    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
        if characteristic.uuid == CBUUID(string: characteristicName) {
            self.peripheral.readValue(for: characteristic)
            if let data = characteristic.value {
                self.sensorValue = data[0]
                self.status = "Value: "+String(self.sensorValue)
            }
        }
    }
    
    func startScanning() {
        self.status = "Scanning BLE devices"
        myCentral.scanForPeripherals(withServices: nil, options: nil)
    }
    func stopScanning() {
        myCentral.stopScan()
    }

}

Then the ContentView is very simple

import SwiftUI

struct ContentView: View {
    @ObservedObject var bleManager = BLEManager()

    var body: some View {
        if bleManager.isSwitchedOn {
            VStack{
                Text(bleManager.status).onAppear(){bleManager.startScanning()}
                .foregroundColor(.green)
            }
        }
        else {
            Text("Bluetooth is NOT swidtched on")
                .foregroundColor(.red)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

where the ObservedObject is updated by the BLE class every time there is a new value from the sensor.

The last important thing to do is to edit the Info.plist file and add Privacy - Bluetooth Always Usage Description with a text value that will be displayed on the Watch when the user will run the App the first time. This is to give to the app the permit to access the Bluetooth.

The full project can be found on git.