Bluetooth Low Energy For IOS Swift Free Chapter
Bluetooth Low Energy For IOS Swift Free Chapter
1st Edition
Tony Gaitatzis
ISBN: 978-1-7751280-5-2
backupbrain.co
i
Bluetooth Low Energy in iOS Swift
by Tony Gaitatzis
All rights reserved. This book or any portion thereof may not be reproduced or used
in any manner whatsoever without the express written permission of the publisher ex-
cept for the use of brief quotations in a book review. For permission requests, write
to the publisher, addressed “Bluetooth iOS Book Reprint Request,” at the address be-
low.
This book contains code samples available under the MIT License, printed below:
Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software, and to permit per-
sons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies
or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EX-
PRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGE-
MENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
ii
Introduction
In this book you will learn the basics of how to program Central and Peripheral de-
vices that communicate over Bluetooth Low Energy using iOS in Swift. These tutori-
als will culminate in three projects:
Through the course of the book you will learn important concepts that relate to:
This book is an excellent read for anyone familiar with iOS programming, who wants
to build an Internet of Things device or a tool that communicates with a Bluetooth de-
vice.
9
Overview
Bluetooth Low Energy (BLE) is a digital radio protocol. Very simply, it works by trans-
mitting radio signals from one computer to another.
The Central has two modes: scanning and connected. The Peripheral has two
modes: advertising and connected. The Peripheral must be advertising for the Cen-
tral to see it.
10
Advertising
A Peripheral advertises by advertising its device name and other information on one
radio frequency, then on another in a process known as frequency hopping. In doing
so, it reduces radio interference created from reflected signals or other devices.
Scanning
Similarly, the Central listens for a server’s advertisement first on one radio frequency,
then on another until it discovers an advertisement from a Peripheral. The process is
not unlike that of trying to find a good show to watch on TV.
The time between radio frequency hops of the scanning Central happens at a differ-
ent speed than the frequency hops of the advertising Peripheral. That way the scan
and advertisement will eventually overlap so that the two can connect.
Each device has a unique media access control address (MAC address) that identi-
fies it on the network. Peripherals advertise this MAC address along with other infor-
mation about the Peripheral’s settings.
Connecting
A Central may connect to a Peripheral after the Central has seen the Peripheral’s ad-
vertisement. The connection involves some kind of handshaking which is handled by
the devices at the hardware or firmware level.
While connected, the Peripheral may not connect to any other device.
11
Disconnecting
A Central may disconnect from a Peripheral at any time. The Peripheral is aware of
the disconnection.
Communication
A Central may send and request data to a Peripheral through something called a
“Characteristic.” Characteristics are provided by the Peripheral for the Central to ac-
cess. A Characteristic may have one or more properties, for example READ or
WRITE. Each Characteristic belongs to a Service, which is like a container for Charac-
teristics. This paradigm is called the Bluetooth Generic Attribute Profile (GATT).
To transmit or request data from a Characteristic, a Central must first connect to the
Characteristic’s Service.
12
For example, a heart rate monitor might have the following GATT profile, allowing a
Central to read the beats per minute, name, and battery life of the server (Figure 1-3).
In order to retrieve the battery life of the Characteristic, the Central must be con-
nected also to the Peripheral’s “Device Info” Service.
13
Byte Order
Bluetooth orders data in both Big-Endian and Little-Endian depending on the con-
text.
During advertisement, data is transmitted in Big Endian, with the most significant
bytes of a number at the end (Figure 1-4).
Data transfers inside the GATT however are transmitted in Little Endian, with the least
significant byte at the end (Figure 1-5).
14
Permissions
A Characteristic grants certain Permissions of the Central. These permissions include
the ability to read and write data on the Characteristic, and to subscribe to Notifica-
tions.
Descriptors
Descriptors describe the configuration of a Characteristic. The only one that has
been specified so far is the “Notification” flag, which lets a Central subscribe to Notifi-
cations.
UUIDs
A UUID, or Universally Unique IDentifier is a very long identifier that is likely to be
unique, no matter when the UUID was created or who created it.
BLE uses UUIDs to label Services and Characteristics so that Services and Character-
istics can be identified accurately even when switching devices or when several Char-
acteristics share the same name.
For example, if a Peripheral has two “Temperature” Characteristics - one for Celsius
and the other in Fahrenheit, UUIDs allow for the right data to be communicated.
ca06ea56-9f42-4fc3-8b75-e31212c97123
But since BLE has very limited data transmission, 16-bit UUIDs are also supported
and can look like this:
0x1815
15
Each Characteristic and each Service is identified by its own UUID. Certain UUIDs
are reserved for specific purposes.
For example, UUID 0x180F is reserved for Services that contain battery reporting
Characteristics.
For example, UUID 0x2A19 is reserved for Characteristics that report battery levels.
A list of UUIDs reserved for specific Services can be found in Appendix IV: Re-
served GATT Services.
If you are unsure what UUIDs to use for a project, you are safe to choose an unas-
signed service (e.g. 0x180C) for a Service and generic Characteristic (0x2A56).
Although the possibility of two generated UUIDs being the same are extremely low,
programmers are free to arbitrarily define UUIDs which may already exist. So long as
the UUIDs defining the Services and Characteristics do not overlap in the a single
GATT Profile, there is no issue in using UUIDs that exist in other contexts.
Bluetooth Hardware
All Bluetooth devices feature at least a processor and an antenna (Figure 1-6).
16
Figure 1-6. Parts of a Bluetooth device
The antenna transmits and receives radio signals. The processor responds to
changes from the antenna and controls the antenna’s tuning, the advertisement mes-
sage, scanning, and data transmission of the BLE device.
As with any radio signal, the quality of the signal drops dramatically with distance, as
shown below (Figure 1-7).
This signal quality is correlated the Received Signal Strength Indicator (RSSI).
17
If the RSSI is known when the Peripheral and Central are 1 meter apart (A), as well as
the RSSI at the current distance (R) and the radio propagation constant (n). The dis-
tance betweeen the Central and the Peripheral in meters (d) can be approximated
with this equation:
A−R
d ≈ 10 10n
The radio propagation constant depends on the environment, but it is typically some-
where between 2.7 in a poor environment and 4.3 in an ideal environment.
Take for example a device with an RSSI of 75 at one meter, a current RSSI reading
35, with a propagation constant of 3.5:
75 − 35
d ≈ 10 10 × 3.5
40
d ≈ 10 35
d ≈ 14
Therefore the distance between the Peripheral and Central is approximately 14 me-
ters.
18
Introducing iOS
Apple has done most of the work necessary to get Bluetooth Low Energy projects off
the ground.
Apple makes it easy for anyone with an Apple computer to get into iOS program-
ming. Xcode is a dream to work with, there are no developer registration costs, and
the Swift programming language is easy to use.
iPhones and iPads, as with all modern mobile devices, are designed to support Blue-
tooth Low Energy.
This book teaches how to make Bluetooth Low Energy (BLE) capable Apps using
Swift for iOS. Although the examples in this book are relatively simple, the app poten-
tial of this technology is amazing.
Xcode Setup
iPhones since versien 5 and and iPads since version 2 are designed to support Blue-
tooth Low Energy.
We will be using XCode to learn how to program Bluetooth Low Energy software with
iOS. Although the examples in this book are relatively simple, the app potential of this
technology is amazing. To program in iOS, you will need XCode on a Mac computer.
19
Search for XCode in the App Store (Figure 2-1).
20
Install XCode by selecting Xcode from the list and clicking the "Install" button (Figure
2-2).
21
Running XCode will open a screen like this. Select "Create a new Xcode project" to
continue (Figure 2-3).
22
Each time a new project is created, the project type must be specified. In this book,
"Single View Application" will be used for all projects (Figure 2-4).
23
From there, a Product Name, Team and Organization Name must be defined. These
names are arbitrary. Names for each chapter project will be suggested (Figure 2-5).
24
Click "Next" and XCode will present a "Save As" modal dialog. Select which folder to
save the new project and click "Create" to save the project (Figure 2-6).
25
The next screen is the project settings, where the Project Name and Team can be
changed. On the left is the project structure, where new classes and groups can be
created (Figure 2-7).
26
The center panel is where code and storyboards are edited (Figure 2-8).
27
Bootstrapping
The first thing to do in any software project is to become familiar with the environ-
ment.
Because we are working with Bluetooth, it’s important to learn how to initialize the
Bluetooth radio and report what the program is doing.
Both the Central and Peripheral talk to the computer over USB when being pro-
grammed. That allows you to report errors and status messages to the computer
when the programs are running (Figure 3-1).
28
Adding Bluetooth Support
Since most Apps don't require Bluetooth, and the APIs take up valueable program
space, the APIs are not included by default. To add support for Bluetooth, the
CoreBluetooth Framework must be included.
This is done by scrolling to the bottom of the Project Settings Screen, to the "Linked
Frameworks and Libraries" section (Figure 3-2).
Click the "+" button to add a new Framework. A dialog will pop up. Search for
"CoreBluetooth" in the search field (Figure 3-3):
29
Figure 3-3. Linked Framework List
Click on "CoreBluetooth.framework" and click the "Add" button to add Bluetooth sup-
port to a project (Figure 3-4).
30
Enable Bluetooth
Before using any Bluetooth features, it is important to import the CoreBluetooth APIs
at in the file header of any class that will use Bluetooth classes, and to turn on the
Bluetooth radio
import CoreBluetooth
The user might turn the Bluetooth radio off any time. Therefore, every time the App
loads, it needs to check if check if Bluetooth is still enabled or has been disabled, us-
ing this function.
The CBCentralManager allows the iOS device to act as a Bluetooth Central, and the
CBCentralManager relays CBCentralManager state changes and events to the local
object.
It takes a moment for Bluetooth to turn on. To prevent trying to access Bluetooth be-
fore it’s ready, the App must listen for the CentralManagerDelegate to respond with a
centralManagerDidUpdateState method, which is triggered by changes in the Blue-
tooth radio status.
31
switch (central.state) {
case .poweredOff:
print ("BLE Hardware is powered off")
bluetoothStatusLabel.text = "Bluetooth Radio Off"
case .poweredOn:
print ("BLE Hardware powered on and ready")
bluetoothStatusLabel.text = "Bluetooth Radio On"
case .resetting:
print ("BLE Hardware is resetting...")
bluetoothStatusLabel.text = "Bluetooth Radio Resetting..."
case .unauthorized:
print ("BLE State is unauthorized")
bluetoothStatusLabel.text = "Bluetooth Radio Unauthorized"
case .unsupported:
print ("Ble hardware is unsupported on this device")
bluetoothStatusLabel.text = "Bluetooth Radio Unsupported"
case .unknown:
print ("Ble state is unavailable")
bluetoothStatusLabel.text = "Bluetooth State Unknown"
}
}
}
32
Table 3-1 . CBManagerState
State Description
Your code structure should now look like this (Figure 3-5).
33
Figure 3-5. Project Structure
Storyboard
34
Figure 3-6. Project Storyboard
Controllers
The ViewController can create a CentralManager which is alerted when the device's
Bluetooth radio is turned on or off.
import UIKit
import CoreBluetooth
/**
35
This view attempts to turn on the Bluetooth Radio
*/
class ViewController: UIViewController, CBCentralManagerDelegate {
// MARK: UI Elements
@IBOutlet weak var bluetoothStatusLabel: UILabel!
/**
View loaded. Start Bluetooth radio.
*/
override func viewDidLoad() {
super.viewDidLoad()
/**
Bluetooth radio state changed
- Parameters:
- central: the reference to the central
*/
func centralManagerDidUpdateState(_ central: CBCentralManager) {
print("Central Manager updated: checking state")
switch (central.state) {
case .poweredOff:
print ("BLE Hardware is powered off")
bluetoothStatusLabel.text = "Bluetooth Radio Off"
case .poweredOn:
36
print ("BLE Hardware powered on and ready")
bluetoothStatusLabel.text = "Bluetooth Radio On"
case .resetting:
print ("BLE Hardware is resetting...")
bluetoothStatusLabel.text = "Bluetooth Radio Resetting..."
case .unauthorized:
print ("BLE State is unauthorized")
bluetoothStatusLabel.text = "Bluetooth Radio Unauthorized"
case .unsupported:
print ("Ble hardware is unsupported on this device")
bluetoothStatusLabel.text = "Bluetooth Radio Unsupported"
case .unknown:
print ("Ble state is unavailable")
bluetoothStatusLabel.text = "Bluetooth State Unknown"
}
}
}
The resulting app will be able to turn the Bluetooth Radio on (Figure 3-7).
37
Figure 3-7. Dialog to request user’s permission to enable Bluetooth and Main
App Screen
38
Import CoreBluetooth
Link the CoreBluetooth Framework from the project Settings (Figure 3-8).
Import the CoreBluetooth library in the code header to access the Bluetooth APIs:
import CoreBluetooth
Enable Bluetooth
let peripheralManager = \
CBPeripheralManager(delegate: self, queue: dispatchQueue)
39
_ peripheral: CBPeripheralManager)
{
switch (state) {
case CBManagerState.poweredOn:
print("Bluetooth on")
case CBManagerState.poweredOff:
print("Bluetooth off")
case CBManagerState.resetting:
print("Bluetooth is resetting")
case CBManagerState.unautharized:
print("App not authorized ot use Bluetooth")
case CBManagerState.unknown:
print("Bluetooth off")
case CBManagerState.poweredOff:
print("Unknown problem when talking trying to start \
Bluetooth radio")
case CBManagerState.unsupported:
print("Bluetooth not supported")
}
}
...
}
It does this by passing a CBManagerState representing the new state of the Blue-
tooth radio, with the following possible states:
40
Table 3-2 . CBManagerState
State Description
poweredO
Bluetooth is disabled
ff
poweredO
Bluetooth is enabled
n
unsupport
Bluetooth is unavailable
ed
41
Figure 3-9. Project Structure
This project will be a single UIView app that creates a custom Bluetooth Peripheral.
Models
The BlePeripheral object will create a custom Bluetooth Peripheral and its track
events and state changes (Example 3-2).
import UIKit
import CoreBluetooth
42
// MARK: Peripheral properties
// Advertized name
let advertisingName = "MyDevice"
// Peripheral Manager
var peripheralManager:CBPeripheralManager!
// Connected Central
var central:CBCentral!
// delegate
var delegate:BlePeripheralDelegate!
/**
Initialize BlePeripheral with a corresponding Peripheral
- Parameters:
- delegate: The BlePeripheralDelegate
- peripheral: The discovered Peripheral
*/
init(delegate: BlePeripheralDelegate?) {
super.init()
// empty dispatch queue
let dispatchQueue:DispatchQueue! = nil
self.delegate = delegate
peripheralManager = \
CBPeripheralManager(delegate: self, queue: dispatchQueue)
}
// MARK: CBPeripheralManagerDelegate
/**
Peripheral will become active
*/
43
func peripheralManager(
_ peripheral: CBPeripheralManager,
willRestoreState dict: [String : Any])
{
print("restoring peripheral state")
}
/**
Bluetooth Radio state changed
*/
func peripheralManagerDidUpdateState(
_ peripheral: CBPeripheralManager)
{
peripheralManager = peripheral
delegate?.blePeripheral?(stateChanged: peripheral.state)
}
}
Delegates
The BlePeripheralDelegate will relay important events from the BlePeripheral to the
ViewController (Example 3-3).
import UIKit
import CoreBluetooth
- Parameters:
- state: new CBManagerState
44
*/
@objc optional func blePeripheral(stateChanged state: CBManagerState)
}
Controllers
The main UIView will instantiate a BlePeripheral object and print a message to the de-
bugger when Bluetooth radio has turned on (Example 3-4).
import UIKit
import CoreBluetooth
// MARK: BlePeripheral
// BlePeripheral
var blePeripheral:BlePeripheral!
/**
UIView loaded
*/
override func viewDidLoad() {
super.viewDidLoad()
}
/**
View appeared. Start the Peripheral
*/
override func viewDidAppear(_ animated: Bool) {
blePeripheral = BlePeripheral(delegate: self)
}
45
// MARK: BlePeripheralDelegate
/**
Bluetooth radio state changed
- Parameters:
- state: the CBManagerState
*/
func blePeripheral(stateChanged state: CBManagerState) {
switch (state) {
case CBManagerState.poweredOn:
print("Bluetooth on")
case CBManagerState.poweredOff:
print("Bluetooth off")
default:
print("Bluetooth not ready yet...")
}
}
}
The resulting app will be able to turn the Bluetooth Radio on (Figure 3-10).
46
Figure 3-10. Main app screen
Example code
The code for this chapter is available online
at: https://github.com/BluetoothLowEnergyIniOSSwift/Chapter03
47
Scanning and Advertising
The first step to any Bluetooth Low Energy interaction is for the Peripheral to make
the Central aware of its existence, through a process called Advertising.
Bluetooth devices discover each other when they are tuned to the same radio fre-
quency, also known as a Channel. There are three channels dedicated to device dis-
covery in Bluetooth Low Energy (Table 4-1):
37 2402 Mhz
39 2426 Mhz
39 2480 Mhz
The peripheral will advertise its name and other data over one channel and then an-
other. This is called frequency hopping (Figure 4-1).
48
Figure 4-1. Advertise and scan processes
Similarly, the Central listens for advertisements first on one channel and then another.
The Central hops frequencies faster than the Peripheral, so that the two are guaran-
teed to be on the same channel eventually.
Peripherals may advertise from 100ms to 100 seconds depending on their configura-
tion, changing channels every 0.625ms (Figure 4-2).
Scanning settings vary wildly, for example scanning every 10ms for 100ms, or scan-
ning for 1 second for 10 seconds.
49
Programming the Central
The previous chapter showed how to access the Bluetooth hardware, specifically the
CBCentralManager. This chapter will show how to scan for Bluetooth devices. This is
done by scanning for Peripherals for a short period of time. During that time any time
a Peripheral is discovered, the system will trigger a callback function. From there dis-
covered Peripheral can be inspected.
If you happen to know one or more Service UUIDs hosted on a Peripheral your App
is seraching for, these UUIDs can be passed into the withServices parameter like
this:
centralManager.stopScan()
It is typical to scan for a period of time before stopping. 3-5 seconds is a reasonable
amount of time to assume that most devices will be discovered during the scanning
process.
func startScan() {
scanCountdown = 5 // 5 seconds
scanTimer = Timer.scheduledTimer(
50
timeInterval: 1.0,
target: self,
selector: #selector(updateScanCounter),
userInfo: nil, repeats: true
)
func updateScanCounter() {
//you code, this is an example
if scanCountdown > 0 {
scanCountdown -= 1
} else {
centralManager?.stopScan()
}
}
centralManager didDiscover can be implemented like this to get the Peripheral identi-
fier:
func centralManager(
_ central: CBCentralManager,
didDiscover peripheral: CBPeripheral,
advertisementData: [String : Any],
rssi RSSI: NSNumber)
{
let peripheralIdentifier = peripheral.identifier
}
51
For security reasons, iOS does not reveal the MAC address of a Peripheral. Instead,
it creates a 32-bit UUID identifier.
52
This example will feature a UITableView to list discovered Peripherals
Models
import UIKit
import CoreBluetooth
// connected Peripheral
var peripheral:CBPeripheral!
// advertised name
var advertisedName:String!
// RSSI
var rssi:NSNumber!
/**
Initialize BlePeripheral with a corresponding Peripheral
- Parameters:
- delegate: The BlePeripheralDelegate
- peripheral: The discovered Peripheral
*/
init(peripheral: CBPeripheral) {
super.init()
self.peripheral = peripheral
}
/**
53
Get a broadcast name from an advertisementData packet.
This may be different than the actual broadcast name
*/
static func getAlternateBroadcastFromAdvertisementData(
advertisementData: [String : Any]) -> String?
{
// grab thekCBAdvDataLocalName from the advertisementData
// to see if there's an alternate broadcast name
if advertisementData["kCBAdvDataLocalName"] != nil {
return (advertisementData["kCBAdvDataLocalName"] as! String)
}
return nil
}
Storyboard
Add a UITableViewCell to it, with the class name "PeripheralTableViewCell" and the
Reuse Identifier of "PeripheralTableViewCell." In the PeripheralTableViewCell, create
and link three UILabels to be used for describing the connected Peripheral properties
(Figure 4-4):
54
Figure 4-4. Project Storyboard
Views
Link the three UILabels from the PeripheralTableViewCell to the corresponding swift
file and create a render function:
import UIKit
import CoreBluetooth
// MARK: UI Elements
@IBOutlet weak var advertisedNameLabel: UILabel!
@IBOutlet weak var identifierLabel: UILabel!
55
@IBOutlet weak var rssiLabel: UILabel!
/**
Render Cell with Peripheral properties
*/
func renderPeripheral(_ blePeripheral: BlePeripheral) {
advertisedNameLabel.text = blePeripheral.advertisedName
identifierLabel.text = \
blePeripheral.peripheral.identifier.uuidString
rssiLabel.text = blePeripheral.rssi.stringValue
}
}
Controllers
The View Controller must be able to initialize a scan when the user clicks the Scan
button. It will scan for Peripherals for 5 seconds, an arbitrarily reasonable scanning
time. The UITableView is updated with each new Peripheral as discovered.
import UIKit
import CoreBluetooth
// MARK: UI Elements
@IBOutlet weak var scanButton: UIButton!
// Default unknown advertisement name
let unknownAdvertisedName = "(UNMARKED)"
// PeripheralTableViewCell reuse identifier
let peripheralCellReusedentifier = "PeripheralTableViewCell"
56
// total scan time
let scanTimeout_s = 5; // seconds
// current countdown
var scanCountdown = 0
// scan timer
var scanTimer:Timer!
// Central Bluetooth Manager
var centralManager:CBCentralManager!
// discovered peripherals
var blePeripherals = [BlePeripheral]()
/**
View loaded. Start Bluetooth radio.
*/
override func viewDidLoad() {
super.viewDidLoad()
print("Initializing central manager")
centralManager = CBCentralManager(delegate: self, queue: nil)
}
/**
User touched the "Scan/Stop" button
*/
@IBAction func onScanButtonTouched(_ sender: UIButton) {
print("scan button clicked")
// if scanning
if scanCountdown > 0 {
stopBleScan()
} else {
startBleScan()
}
}
/**
Scan for Bluetooth peripherals
57
*/
func startBleScan() {
scanButton.setTitle("Stop", for: UIControlState.normal)
blePeripherals.removeAll()
tableView.reloadData()
print ("discovering devices")
scanCountdown = scanTimeout_s
scanTimer = Timer.scheduledTimer(
timeInterval: 1.0,
target: self,
selector: #selector(updateScanCounter),
userInfo: nil,
repeats: true)
if let centralManager = centralManager {
centralManager.scanForPeripherals(
withServices: nil,
options: nil)
}
}
/**
Stop scanning for Bluetooth Peripherals
*/
func stopBleScan() {
if let centralManager = centralManager {
centralManager.stopScan()
}
scanTimer.invalidate()
scanCountdown = 0
scanButton.setTitle("Start", for: UIControlState.normal)
}
/**
Update the scan countdown timer
*/
58
func updateScanCounter() {
//you code, this is an example
if scanCountdown > 0 {
print("\(scanCountdown) seconds until Ble Scan ends")
scanCountdown -= 1
} else {
stopBleScan()
}
}
/**
New Peripheral discovered
- Parameters
- central: the CentralManager for this UIView
- peripheral: a discovered Peripheral
- advertisementData: the Bluetooth GAP data discovered
- rssi: the radio signal strength indicator for this Peripheral
*/
func centralManager(
_ central: CBCentralManager,
didDiscover peripheral: CBPeripheral,
advertisementData: [String : Any],
rssi RSSI: NSNumber)
{
print("Discovered \(peripheral.identifier.uuidString) " + \
"(\(peripheral.name))")
59
peripheralFound = true
break
}
}
/**
Bluetooth radio state changed
60
- Parameters:
- central: the reference to the central
*/
func centralManagerDidUpdateState(_ central: CBCentralManager) {
print("Central Manager updated: checking state")
switch (central.state) {
case .poweredOn:
print("BLE Hardware powered on and ready")
scanButton.isEnabled = true
default:
print("Bluetooth unavailable")
}
}
/**
return number of sections. Only 1 is needed
*/
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
/**
Return number of Peripheral cells
*/
override func tableView(
_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int
{
return blePeripherals.count
}
/**
Return rendered Peripheral cell
61
*/
override func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
print("setting up table cell")
let cell = tableView.dequeueReusableCell(
withIdentifier: peripheralCellReusedentifier,
for: indexPath) as! PeripheralTableViewCell
// fetch the appropritae peripheral for the data source layout
let peripheral = blePeripherals[indexPath.row]
cell.renderPeripheral(peripheral)
return cell
}
}
Compile and run the app. When it runs, you will see a screen with a scan button.
When the scan button is clicked, it locates your BLE device (Figure 4-5).
62
Figure 4-5. App screen prior to a Bluetooth scan and after discovering Blue-
tooth Peripherals
This chapter will show how to advertise a Bluetooth Low Energy Peripheral.
63
Advertising is simple. Create a Dictionary of advertising parameters and pass the Dic-
tionary into the CBPeripheralManager.startAdvertising method:
CBAdvertisementDataServiceUUI
DsKey [CBUUID] UUIDs of listed Services
As this chapter is focused on Advertising, only the first parameter will be discussed.
func peripheralManagerDidStartAdvertising(
_ peripheral: CBPeripheralManager,
error: Error?)
{
if error != nil {
print ("Error advertising peripheral")
print(error.debugDescription)
}
// store a copy of the updated Peripheral
64
peripheralManager = peripheral
}
peripheralManager.stopAdvertising()
This example will add a UISwitch that shows when a Peripheral has begun Advertis-
ing.
Custom scanner callbacks will be created, which respond to events when scanning
has stopped.
Models
// Advertized name
let advertisingName = "MyDevice"
...
/**
65
Stop advertising, shut down the Peripheral
*/
func stop() {
peripheralManager.stopAdvertising()
}
/**
Start Bluetooth Advertising.
*/
func startAdvertising() {
let advertisementData:[String: Any] = [
CBAdvertisementDataLocalNameKey: advertisingName
]
peripheralManager.startAdvertising(advertisementData)
}
// MARK: CBPeripheralManagerDelegate
/**
Peripheral started advertising
*/
func peripheralManagerDidStartAdvertising(
_ peripheral: CBPeripheralManager,
error: Error?)
{
if error != nil {
print ("Error advertising peripheral")
print(error.debugDescription)
}
self.peripheralManager = peripheral
delegate?.blePerihperal?(startedAdvertising: error)
}
...
}
66
Delegates
...
/**
BlePeripheral statrted adertising
- Parameters:
- error: the error message, if any
*/
@objc optional func blePerihperal(startedAdvertising error: Error?)
...
Storyboard
Add a UILabel to show the Peripheral's Advertised name and a UISwitch to show the
Advertising state of the Peripheral (Figure 4-6):
67
Figure 4-6. Project Storyboard
Controllers
Add a UILabel to display the Advertising name and a UISwitch to show the Advertis-
ing state of the Peripheral. Add functionality in the viewDidAppear, viewWillLoad, and
viewDidDisappear to change the text on the UILabel and the state of the UISwitch to
reflect the state of the Peripheral. And add a callback handler for the new BlePeripher-
alDelegate method:
...
// MARK: UI Elements
@IBOutlet weak var advertisingLabel: UILabel!
@IBOutlet weak var advertisingSwitch: UISwitch!
...
68
/**
View appeared. Start the Peripheral
*/
override func viewDidAppear(_ animated: Bool) {
blePeripheral = BlePeripheral(delegate: self)
advertisingLabel.text = blePeripheral.advertisingName
}
/**
View will appear. Stop transmitting random data
*/
override func viewWillDisappear(_ animated: Bool) {
blePeripheral.stop()
}
/**
View disappeared. Stop advertising
*/
override func viewDidDisappear(_ animated: Bool) {
advertisingSwitch.setOn(false, animated: true)
}
...
/**
BlePeripheral statrted adertising
- Parameters:
- error: the error message, if any
*/
func blePerihperal(startedAdvertising error: Error?) {
if error != nil {
print("Problem starting advertising: " + error.debugDescription)
} else {
print("adertising started")
advertisingSwitch.setOn(true, animated: true)
}
}
69
...
Compile and run the app. When it runs, a Bluetooth Peripheral will be advertising (Fig-
ure 4-7).
70
Example code
The code for this chapter is available online
at: https://github.com/BluetoothLowEnergyIniOSSwift/Chapter04
71
Connecting
Each Bluetooth Device has a unique Media Access Control (MAC) address, a 48-bit
identifier value written like this
00:A0:C9:14:C8:3A
Devices advertise data on the network with the intended recipient's MAC address at-
tached so that recipient devices can filter data packets that are intended for them.
For security reasons, iOS does not reveal the MAC address of a Peripheral. Instead,
it creates a 32-bit UUID identifier, like this:
2fe058bd-5edb-4b3f-b7bd-fc8e93e2dbc4
Once a Central has discovered a Peripheral, the central can attempt to connect. This
must be done before data can be passed between the Central and Peripheral. A Cen-
tral may hold several simultaneous connections with a number of peripherals, but a
Peripheral may only hold one connection at a time. Hence the names Central and Pe-
ripheral (Figure 5-1).
72
Bluetooth supports data 37 data channels ranging from 2404 MHz to 2478 MHz.
Once the connection is established, the Central and Peripheral negotiate which of
these channels to begin communicating over.
Because the Peripheral can only hold one connection at a time, it must disconnect
from the Central before a new connection can be made.
The connection and disconnection process works like this (Figure 5-2).
centralManager.connect(peripheral)
73
If the connection is successful, the didConnect callback will be triggered.
func centralManager(
_ central: CBCentralManager,
didConnect peripheral: CBPeripheral)
{
}
func centralManager(
_ central: CBCentralManager,
didFailToConnect peripheral: CBPeripheral,
error: Error?)
{
}
centralManager.cancelPeripheralConnection(peripheral)
func centralManager(
_ central: CBCentralManager,
didDisconnectPeripheral peripheral: CBPeripheral,
error: Error?)
{
}
74
Disconnecting is important. The Peripheral can only be connected to one device at a
time. Sometimes, closing an Activity without disconnecting the Peripheral can leave
the Peripheral in a connected state - unable to advertise or connect to a Central
again in the future.
75
Figure 5-3. Added project structure
Models
import UIKit
import CoreBluetooth
76
class BlePeripheral: NSObject, CBPeripheralDelegate {
// MARK: Peripheral properties
// delegate
var delegate:BlePeripheralDelegate?
// connected Peripheral
var peripheral:CBPeripheral!
// advertised name
var advertisedName:String!
// RSSI
var rssi:NSNumber!
/**
Initialize BlePeripheral with a corresponding Peripheral
- Parameters:
- delegate: The BlePeripheralDelegate
- peripheral: The discovered Peripheral
*/
init(delegate: BlePeripheralDelegate?, peripheral: CBPeripheral) {
super.init()
self.peripheral = peripheral
self.peripheral.delegate = self
self.delegate = delegate
}
/**
Notify the BlePeripheral that the peripheral has been connected
- Parameters:
- peripheral: The discovered Peripheral
*/
func connected(peripheral: CBPeripheral) {
self.peripheral = peripheral
self.peripheral.delegate = self
// check for services and the RSSI
77
self.peripheral.readRSSI()
}
/**
Get a broadcast name from an advertisementData packet.
This may be different than the actual broadcast name
*/
static func getAlternateBroadcastFromAdvertisementData(
advertisementData: [String : Any]) -> String?
{
// grab thekCBAdvDataLocalName from the advertisementData
// to see if there's an alternate broadcast name
if advertisementData["kCBAdvDataLocalName"] != nil {
return (advertisementData["kCBAdvDataLocalName"] as! String)
}
return nil
}
/**
Determine if this peripheral is connectable
from it's advertisementData packet.
*/
static func isConnectable(advertisementData: [String: Any]) -> Bool {
let isConnectable = \
advertisementData["kCBAdvDataIsConnectable"] as! Bool
return isConnectable
}
// MARK: CBPeripheralDelegate
/**
RSSI read from peripheral.
*/
func peripheral(
_ peripheral: CBPeripheral,
didReadRSSI RSSI: NSNumber,
78
error: Error?)
{
print("RSSI: \(RSSI.stringValue)")
rssi = RSSI
delegate?.blePeripheral?(readRssi: rssi, blePeripheral: self)
}
}
Delegates
import UIKit
import CoreBluetooth
- Parameters:
- rssi: the RSSI
- blePeripheral: the BlePeripheral
*/
@objc optional func blePeripheral(
readRssi rssi: NSNumber,
blePeripheral: BlePeripheral)
}
79
Storyboard
Create a new UIView and give it the class name "PeripheralViewController." Add a se-
gue between the two UIViews. Create and link three UILabels to be used for describ-
ing the connected Peripheral properties (Figure 5-4):
Controllers
The previous app was able to detect and list nearby Peripherals. This app will allow
connecting to a Peripheral when a user selects that Peripheral from the UITableView.
80
Example 5-3. UI/Controllers/PeripheralTableViewController.swift
...
override func tableView(
_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath)
{
stopBleScan()
let selectedRow = indexPath.row
print("Row: \(selectedRow)")
print(blePeripherals[selectedRow])
}
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let peripheralViewController = \
segue.destination as! PeripheralViewController
if let selectedIndexPath = tableView.indexPathForSelectedRow {
let selectedRow = selectedIndexPath.row
if selectedRow < blePeripherals.count {
// prepare next UIView
peripheralViewController.centralManager = centralManager
peripheralViewController.blePeripheral = \
blePeripherals[selectedRow]
}
tableView.deselectRow(at: selectedIndexPath, animated: true)
}
}
}
81
Example 5-4. UI/Controllers/PeripheralViewController.swift
import UIKit
import CoreBluetooth
// MARK: UI Elements
@IBOutlet weak var advertisedNameLabel: UILabel!
@IBOutlet weak var identifierLabel: UILabel!
@IBOutlet weak var rssiLabel: UILabel!
// Central Manager
var centralManager:CBCentralManager!
// connected Peripheral
var blePeripheral:BlePeripheral!
/**
UIView loaded
*/
override func viewDidLoad() {
super.viewDidLoad()
print("Will connect to " + \
"\(blePeripheral.peripheral.identifier.uuidString)")
// Assign delegates
blePeripheral.delegate = self
centralManager.delegate = self
centralManager.connect(blePeripheral.peripheral)
/**
RSSI discovered. Update UI
82
*/
func blePeripheral(
readRssi rssi: NSNumber,
blePeripheral: BlePeripheral)
{
rssiLabel.text = rssi.stringValue
}
/**
Peripheral connected. Update UI
*/
func centralManager(
_ central: CBCentralManager,
didConnect peripheral: CBPeripheral)
{
print("Connected Peripheral: \(peripheral.name)")
advertisedNameLabel.text = blePeripheral.advertisedName
identifierLabel.text =\
blePeripheral.peripheral.identifier.uuidString
blePeripheral.connected(peripheral: peripheral)
}
/**
Connection to Peripheral failed.
*/
func centralManager(
_ central: CBCentralManager,
didFailToConnect peripheral: CBPeripheral,
error: Error?)
{
print("failed to connect")
print(error.debugDescription)
}
83
/**
Peripheral disconnected. Leave UIView
*/
func centralManager(
_ central: CBCentralManager,
didDisconnectPeripheral peripheral: CBPeripheral,
error: Error?)
{
print("Disconnected Peripheral: \(peripheral.name)")
dismiss(animated: true, completion: nil)
}
/**
Bluetooth radio state changed.
*/
func centralManagerDidUpdateState(_ central: CBCentralManager) {
print("Central Manager updated: checking state")
switch (central.state) {
case .poweredOn:
print("bluetooth on")
default:
print("bluetooth unavailable")
}
}
// MARK: Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
print("leaving view - disconnecting from peripheral")
if let peripheral = blePeripheral.peripheral {
centralManager.cancelPeripheralConnection(peripheral)
}
}
}
84
The resulting App will be one that can scan and connect to an advertising Peripheral
(Figure 5-5).
85
Peripheral Programming
In iOS, no notifications are sent when a Peripheral is connected to. There Peripheral
code remains the same as the previous chapter.
Example code
The code for this chapter is available online
at: https://github.com/BluetoothLowEnergyIniOSSwift/Chapter05
86
Services and Characteristics
Before data can be transmitted back and forth between a Central and Peripheral, the
Peripheral must host a GATT Profile. That is, the Peripheral must have Services and
Characteristics.
Some UUIDs are reserved for specific use. For instance any Characteristic with the
16-bit UUID 0x2a35 (or the 32-bit UUID 00002a35-0000-1000-8000-00805f9b34fb) is
implied to be a blood pressure reading.
For a list of reserved Service UUIDs, see Appendix IV: Reserved GATT Services.
For a list of reserved Characteristic UUIDs, see Appendix V: Reserved GATT Char-
acteristics.
87
Service/
Characterstic
Characterstic
Characterstic
Service/
Characterstic
Characterstic
Characterstic
Characteristics act as channels that can be communicated on, and Services act as
containers for Characteristics. A top level Service is called a Primary service, and a
Service that is within another Service is called a Secondary Service.
Permissions
Characteristics can be configured with the following attributes, which define what the
Characteristic is capable of doing (Table 6-1):
88
Table 6-1. Characteristic Permissions
Descriptor Description
Read Central can read this Characteristic, Peripheral can set the value.
Because the GATT Profile is hosted on the Peripheral, the terms used to describe a
Characteristic’s permissions are relative to how the Peripheral accesses that Charac-
teristic. Therefore, when a Central uploads data to the Peripheral, the Peripheral can
“read” from the Characteristic. The Peripheral “writes” new data to the Characteris-
tic, and can “notify” the Central that the data is altered.
89
The Central can be programmed to read the GATT Profile of the Peripheral after con-
nection, like this:
peripheral.discoverServices(nil)
If only a subset of the Services hosted by the Peripheral are needed, those Service
UUIDs can be passed into the discoverServices function like this:
When the Services are discovered, a callback will be executed by the CBPeripheral-
ManagerDelegate, containing an updated CBPeripheral object. This updated object
contains an array of Services:
func peripheral(
_ peripheral: CBPeripheral,
didDiscoverServices error: Error?)
{
if error != nil {
print("Discover service Error: \(error)")
}
}
In order for the class to access these methods, it must implement CBPeripheralMana-
gerDelegate.
There are Primary Services and Secondary services. Secondary Services are con-
tained within other Services; Primary Services are not. The type of Service can be dis-
covered by inspecting the CBService.isPrimary flag.
90
To discover the Characteristics hosted by these services, simply loop through the dis-
covered Services and handle the resulting peripheral didDiscoverCharacteristicsFor
callback:
func peripheral(
_ peripheral: CBPeripheral,
didDiscoverServices error: Error?)
{
if error != nil {
print("Discover service Error: \(error)")
} else {
for service in peripheral.services!{
self.peripheral.discoverCharacteristics(nil, for: service)
}
}
}
func peripheral(
_ peripheral: CBPeripheral,
didDiscoverCharacteristicsFor service: CBService,
error: Error?)
{
let serviceIdentifier = service.uuid.uuidString
if let characteristics = service.characteristics {
for characteristic in characteristics {
// do something with Characteristic
}
}
}
Each Characteristic has certain permission properties that allow the Central to read,
write, or receive notifications from it (Table 6-2).
91
Table 6-2. CBCharacteristicProperties
Permissi
Value Description
on
In iOS, these properties are expressed as a binary integer which can be extracted like
this:
A Note on Caching
Because Bluetooth was designed to be a low-power protocol, measures are taken to
limit redundancy and power consumption through radio and CPU usage. As a result,
a Peripheral’s GATT Profile is cached on iOS. This is not a problem for normal use,
92
but when developing, it can be confusing to change Characteristic permissions and
not see the updates reflected on iOS.
To get around this, the iOS device must be restarted each time a Peripheral with the
same Identifier has has changed its GATT Profile
Create a new project called Services and copy everything from the previous example.
(Figure 6-3).
93
Figure 6-3. Project Structure
Objects
...
/**
Servicess were discovered on the connected Peripheral
*/
func peripheral(
_ peripheral: CBPeripheral,
94
didDiscoverServices error: Error?)
{
print("services discovered")
// clear GATT profile - start with fresh services listing
gattProfile.removeAll()
if error != nil {
print("Discover service Error: \(error)")
} else {
print("Discovered Service")
for service in peripheral.services!{
self.peripheral.discoverCharacteristics(nil, for: service)
}
print(peripheral.services!)
}
}
/**
Characteristics were discovered
for a Service on the connected Peripheral
*/
func peripheral(
_ peripheral: CBPeripheral,
didDiscoverCharacteristicsFor service: CBService,
error: Error?)
{
print("characteristics discovered")
// grab the service
let serviceIdentifier = service.uuid.uuidString
print("service: \(serviceIdentifier)")
gattProfile.append(service)
if let characteristics = service.characteristics {
print("characteristics found: \(characteristics.count)")
for characteristic in characteristics {
print("-> \(characteristic.uuid.uuidString)")
}
95
delegate?.blePerihperal?(
discoveredCharacteristics: characteristics,
forService: service,
blePeripheral: self)
}
}
...
Delegates
...
/**
Characteristics were discovered for a Service
- Parameters:
- characteristics: the Characteristic list
- forService: the Service these Characteristics are under
- blePeripheral: the BlePeripheral
*/
@objc optional func blePerihperal(
discoveredCharacteristics characteristics: [CBCharacteristic],
forService: CBService,
blePeripheral: BlePeripheral)
...
96
Storyboard
Views
The GATT Profile will be represented as a Grouped UITableView, with Services as the
table header and Characteristics as the GattTableViewCell table cell.
97
Example 6-3. UI/Views/GattTableViewCell.swift
import UIKit
import CoreBluetooth
Controllers
Add functionality in the PeripheralViewController to render the GATT Profile table and
to handle the blePeripheral discoveredCharacteristics callback from the BlePeripheral-
Delegate class:
// MARK: UI Elements
@IBOutlet weak var advertisedNameLabel: UILabel!
@IBOutlet weak var identifierLabel: UILabel!
@IBOutlet weak var rssiLabel: UILabel!
@IBOutlet weak var gattProfileTableView: UITableView!
@IBOutlet weak var gattTableView: UITableView!
98
// MARK: BlePeripheralDelegate
/**
Characteristics were discovered. Update the UI
*/
func blePerihperal(
discoveredCharacteristics characteristics: [CBCharacteristic],
forService: CBService,
blePeripheral: BlePeripheral)
{
gattTableView.reloadData()
}
/**
RSSI discovered. Update UI
*/
func blePeripheral(
readRssi rssi: NSNumber,
blePeripheral: BlePeripheral)
{
rssiLabel.text = rssi.stringValue
}
// MARK: UITableViewDataSource
/**
Return number of rows in Service section
*/
func tableView(
_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int
{
print("returning num rows in section")
if section < blePeripheral.gattProfile.count {
if let characteristics = \
blePeripheral.gattProfile[section].characteristics
99
{
return characteristics.count
}
}
return 0
}
/**
Return a rendered cell for a Characteristic
*/
func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
print("returning table cell")
let cell = tableView.dequeueReusableCell(
withIdentifier: gattCellReuseIdentifier,
for: indexPath) as! GattTableViewCell
let section = indexPath.section
let row = indexPath.row
/**
Return the number of Service sections
100
*/
func numberOfSections(in tableView: UITableView) -> Int {
print("returning number of sections")
print(blePeripheral)
print(blePeripheral.gattProfile)
return blePeripheral.gattProfile.count
}
/**
Return the title for a Service section
*/
func tableView(
_ tableView: UITableView,
titleForHeaderInSection section: Int) -> String?
{
print("returning title at section \(section)")
if section < blePeripheral.gattProfile.count {
return blePeripheral.gattProfile[section].uuid.uuidString
}
return nil
}
/**
User selected a Characteristic table cell.
Update UI and open the next UIView
*/
func tableView(
_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath)
{
let selectedRow = indexPath.row
print("Selected Row: \(selectedRow)")
tableView.deselectRow(at: indexPath, animated: true)
}
101
// MARK: Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
print("leaving view - disconnecting from peripheral")
if let peripheral = blePeripheral.peripheral {
centralManager.cancelPeripheralConnection(peripheral)
}
}
}
The resulting app will be able to connect to a Peripheral and list the Services and
Characteristics (Figure 6-5).
102
Figure 6-5. App screen showing GATT profile from a connected Peripheral
103
// Service UUID
let serviceUuid = CBUUID(string: "0000180c-0000-1000-8000-00805f9b34fb")
let service = CBMutableService(type: serviceUuid, primary: true)
peripheralManager.add(service)
func peripheralManager(
_ peripheral: CBPeripheralManager,
didAdd service: CBService, error: Error?)
{
}
Permissi
Value Description
on
104
Create the Characteristics properties by instanciating and merging CBCharacteris-
ticProperties:
Value Description
105
Create a new CBMutableCharacteristic with the defined properties. Optionally an ini-
tial value can be set.
let characteristicUuid = \
CBUUID(string: "00002a56-0000-1000-8000-00805f9b34fb")
var value:Data!
// instantiate a Characteristic
let characteristic = CBMutableCharacteristic(
type: characteristicUuid,
properties: characteristicProperties,
value: value,
permissions: characterisitcPermissions)
106
Figure 6-6. Minimal GATT Profile for Peripherals
This provides Central software, surveying tools, and future developers to better under-
stand what each Peripheral is, how to interact with it, and what the battery capabili-
ties are.
For pedagogical reasons, many of the examples will not include this portion of the
GATT Profile.
Models
Modify BlePeripheral.swift to build a minimal Gatt Services profile. Build the GATT
Profile structure, and handle the callback when Services are added.
...
107
// MARK: GATT Profile
// Service UUID
let serviceUuid = CBUUID(string: "0000180c-0000-1000-8000-00805f9b34fb")
// Characteristic UUIDs
let characteristicUuid = CBUUID(
string: "00002a56-0000-1000-8000-00805f9b34fb")
// Read Characteristic
var characteristic:CBMutableCharacteristic!
...
/**
Build Gatt Profile.
This must be done after Bluetooth Radio has turned on
*/
func buildGattProfile() {
let service = CBMutableService(type: serviceUuid, primary: true)
var characteristicProperties = CBCharacteristicProperties.read
characteristicProperties.formUnion(
CBCharacteristicProperties.notify)
var characterisitcPermissions = CBAttributePermissions.writeable
characterisitcPermissions.formUnion(CBAttributePermissions.readable)
characteristic = CBMutableCharacteristic(
type: characteristicUuid,
properties: characteristicProperties,
value: nil,
permissions: characterisitcPermissions)
service.characteristics = [ characteristic ]
peripheralManager.add(service)
}
...
/**
Peripheral added a new Service
*/
func peripheralManager(
_ peripheral: CBPeripheralManager,
108
didAdd service: CBService,
error: Error?)
{
print("added service to peripheral")
if error != nil {
print(error.debugDescription)
}
}
/**
Bluetooth Radio state changed
*/
func peripheralManagerDidUpdateState(
_ peripheral: CBPeripheralManager)
{
peripheralManager = peripheral
switch peripheral.state {
case CBManagerState.poweredOn:
buildGattProfile()
startAdvertising()
default: break
}
delegate?.blePeripheral?(stateChanged: peripheral.state)
}
...
Example code
The code for this chapter is available online
at: https://github.com/BluetoothLowEnergyIniOSSwift/Chapter06
109
Reading Data from a Peripheral
The real value of Bluetooth Low Energy is the ability to transmit data wirelessly.
Bluetooth Peripherals are passive, so they don’t push data to a connected Central.
Instead, Centrals make a request to read data from a Characteristic. This can only
happen if the Characteristic enables the Read Attribute.
110
Programming the Central
Before reading data from a connected Peripheral, it may be useful to know if a Char-
acteristic provides read permission. Read permission can be read by getting the Char-
acteristic property bit map and isolating the read property from it, like this:
Once the Central has a Bluetooth GATT connection and has access to a Characteris-
tic with which to communicate with a connected Peripheral, the Central can request
to read data from that Characteristic like this:
peripheral.readValue(for: characteristic)
This will initiate a read request from the Central to the Peripheral.
When the Central finishes reading data from the Peripheral’s Characteristic, the pe-
ripheral didUpdateValueFor method is triggered in the CBPeripheralManagerDele-
gate.
In this callback, the Characteristic’s value can read as a Data object using the
characteristic.value property.
func peripheral(
_ peripheral: CBPeripheral,
didUpdateValueFor characteristic: CBCharacteristic,
error: Error?)
{
let value = characteristic.value
}
From here the data can be converted into any format, including a String or an Integer.
111
// convert to byte array
let byteArray = [UInt8](value)
// convert to String
let stringValue = String(data: value, encoding: .ascii)
// convert to float
let floatValue = value.withUnsafeBytes {
(ptr: UnsafePointer<Float>) -> Float in
return ptr.pointee
}
112
Figure 7-2. Added project files
Models
...
/**
Read from a Characteristic
113
*/
func readValue(from characteristic: CBCharacteristic) {
self.peripheral.readValue(for: characteristic)
}
/**
Check if Characteristic is readable
- Parameters:
- characteristic: The Characteristic to test
// MARK: CBPeripheralDelegate
/**
Value downloaded from Characteristic on connected Peripheral
*/
func peripheral(
_ peripheral: CBPeripheral,
didUpdateValueFor characteristic: CBCharacteristic,
error: Error?)
{
print("characteristic updated")
if let value = characteristic.value {
114
print(value.debugDescription)
print(value.description)
Delegates
...
/**
Characteristic was read
- Parameters:
- stringValue: the value read from the Charactersitic
- characteristic: the Characteristic that was read
- blePeripheral: the BlePeripheral
*/
@objc optional func blePeripheral(
characteristicRead stringValue: String,
characteristic: CBCharacteristic,
blePeripheral: BlePeripheral)
115
...
Storyboard
Create a new UIView, with class name "CharacteristicViewController" and a new se-
gue to it from the PeripheralViewController. Create and link three UILabel in the Gatt-
TableViewCell to show the Characteristic UUID and read/no access properties. Cre-
ate and link three UILabels to show the Peripheral and Characteristic identifiers, plus
a UIBUtton and UITextView to allow the user to trigger a Characteristic read and dis-
play the result on screen (Figure 7-3):
116
Views
import UIKit
import CoreBluetooth
// MARK: UI Elements
@IBOutlet weak var uuidLabel: UILabel!
@IBOutlet weak var readableLabel: UILabel!
@IBOutlet weak var noAccessLabel: UILabel!
/**
Render the cell with Characteristic properties
*/
func renderCharacteristic(characteristic: CBCharacteristic) {
uuidLabel.text = characteristic.uuid.uuidString
let isReadable = \
BlePeripheral.isCharacteristic(isReadable: characteristic)
readableLabel.isHidden = !isReadable
if isReadable {
noAccessLabel.isHidden = true
} else {
noAccessLabel.isHidden = false
}
}
}
117
Controllers
Create a new view controller, CharacteristicViewController that will interact with a se-
lected Charactersitic. It will display the properties of the Characteristic and allow the
user to trigger a read event on the BlePeripheral. The Characteristic's value will be
displayed in a UITextView.
import UIKit
import CoreBluetooth
// MARK: UI elements
@IBOutlet weak var advertizedNameLabel: UILabel!
@IBOutlet weak var identifierLabel: UILabel!
@IBOutlet weak var characteristicUuidlabel: UILabel!
@IBOutlet weak var readCharacteristicButton: UIButton!
@IBOutlet weak var characteristicValueText: UITextView!
/**
UIView loaded
*/
118
override func viewDidLoad() {
super.viewDidLoad()
print("Will connect to device " + \
"\(blePeripheral.peripheral.identifier.uuidString)")
print("Will connect to characteristic " + \
"\(connectedCharacteristic.uuid.uuidString)")
centralManager.delegate = self
blePeripheral.delegate = self
loadUI()
}
/**
Load UI elements
*/
func loadUI() {
advertizedNameLabel.text = blePeripheral.advertisedName
identifierLabel.text = \
blePeripheral.peripheral.identifier.uuidString
characteristicUuidlabel.text = \
connectedCharacteristic.uuid.uuidString
readCharacteristicButton.isEnabled = true
// characteristic is not readable
if !BlePeripheral.isCharacteristic(
isReadable: connectedCharacteristic) {
readCharacteristicButton.isHidden = true
characteristicValueText.isHidden = true
}
}
/**
User touched Read button. Request to read the Characteristic
*/
@IBAction func onReadCharacteristicButtonTouched(_ sender: UIButton) {
print("pressed button")
readCharacteristicButton.isEnabled = false
blePeripheral.readValue(from: connectedCharacteristic)
119
}
// MARK: BlePeripheralDelegate
/**
Characteristic was read. Update UI
*/
func blePeripheral(
characteristicRead stringValue: String,
characteristic: CBCharacteristic,
blePeripheral: BlePeripheral)
{
print(stringValue)
readCharacteristicButton.isEnabled = true
characteristicValueText.insertText(stringValue + "\n")
let stringLength = characteristicValueText.text.characters.count
characteristicValueText.scrollRangeToVisible(NSMakeRange(
stringLength-1, 0))
}
// MARK: CBCentralManagerDelegate
/**
Peripheral disconnected
- Parameters:
- central: the reference to the central
- peripheral: the connected Peripheral
*/
func centralManager(
_ central: CBCentralManager,
didDisconnectPeripheral peripheral: CBPeripheral,
error: Error?)
{
// disconnected. Leave
print("disconnected")
120
if let navController = navigationController {
navController.popToRootViewController(animated: true)
dismiss(animated: true, completion: nil)
}
/**
Bluetooth radio state changed
- Parameters:
- central: the reference to the central
*/
func centralManagerDidUpdateState(_ central: CBCentralManager) {
print("Central Manager updated: checking state")
switch (central.state) {
case .poweredOn:
print("bluetooth on")
default:
print("bluetooth unavailable")
}
}
// MARK: - Navigation
/**
Animate the segue
*/
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller
// using segue.destinationViewController.
// Pass the selected object to the new view controller.
if let connectedBlePeripheral = blePeripheral {
centralManager.cancelPeripheralConnection(
connectedBlePeripheral.peripheral)
}
121
}
}
...
/**
User selected a Characteristic table cell.
Update UI and open the next UIView
*/
func tableView(
_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath)
{
let selectedRow = indexPath.row
print("Selected Row: \(selectedRow)")
}
// MARK: Navigation
/**
Handle the Segue. Prepare the next UIView with necessary information
*/
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
print("leaving view - disconnecting from peripheral")
122
if selectedSection < blePeripheral.gattProfile.count {
let service = blePeripheral.gattProfile[selectedSection]
if let characteristics = \
blePeripheral.gattProfile[selectedSection].\
characteristics
{
if selectedRow < characteristics.count {
// populate next UIView with necessary information
characteristicViewController.centralManager = \
centralManager
characteristicViewController.blePeripheral = \
blePeripheral
characteristicViewController.connectedService = \
service
characteristicViewController.\
connectedCharacteristic = \
characteristics[selectedRow]
}
}
}
gattTableView.deselectRow(at: indexPath, animated: true)
} else {
if let peripheral = blePeripheral.peripheral {
centralManager.cancelPeripheralConnection(peripheral)
}
}
}
}
When run, the App will be able to scan for and connect to a Peripheral. Once con-
nected, it can list the GATT Profile - the Services and Characteristics hosted on the
Peripheral. A Characteristic can be selected and values can be read from that Charac-
teristic (Figure 7-4).
123
Figure 7-4. App screens showing GATT Profile for connected Peripheral and val-
ues read from a Characteristic on a connected Peripheral
124
string: "00002a56-0000-1000-8000-00805f9b34fb")
// Make Characteristic readable
let characteristicProperties = CBCharacteristicProperties.read
// Make Attributes readable
let characterisitcPermissions = CBAttributePermissions.readable
// create the readable Characteristic
let readCharacteristic = CBMutableCharacteristic(
type: readCharacteristicUuid, properties: characteristicProperties,
value: nil,
permissions: characterisitcPermissions)
// set the Characteristic as the only one in the Service
service.characteristics = [ readCharacteristic ]
When the Central requests to read data from the Peripheral’s Characteristic, the pe-
ripheralManager didReceiveRead method is triggered the CBPeripheralManagerDele-
gate object.
func peripheralManager(
_ peripheral: CBPeripheralManager,
didReceiveRead request: CBATTRequest)
{
}
The Central cannot read the Characteristic value until the request's value is set:
func peripheralManager(
_ peripheral: CBPeripheralManager,
didReceiveRead request: CBATTRequest)
{
let characteristic = request.characteristic
if let value = characteristic.value {
// Respond to the Central with the Characteristic value
let range = Range(uncheckedBounds: (
lower: request.offset,
125
upper: value.count - request.offset))
request.value = value.subdata(in: range)
}
}
The Central needs to know if the request was successful. This is done by responding
to the with a CBATTError status.
Value Description
requestNotSupport
Request was not supported.
ed
...
peripheral.respond(to: request, withResult: CBATTError.success)
...
func peripheralManager(
_ peripheral: CBPeripheralManager,
didReceiveRead request: CBATTRequest)
{
126
let characteristic = request.characteristic
if let value = characteristic.value {
// if the requested offset is
// larger than the Characteristic value, send an error
if request.offset > value.count {
peripheralManager.respond(
to: request,
withResult: CBATTError.invalidOffset)
return
}
// Respond to the Central with the Characteristic value
let range = Range(uncheckedBounds: (
lower: request.offset,
upper: value.count - request.offset))
request.value = value.subdata(in: range)
Models
Modify the BlePeripheral class to include build a readable Characteristic, sets a ran-
dom String to the Characteristic every 5 seconds, and responds when the Central ini-
tiates a read request:
127
Example 7-6. Models/BlePeripheral.swift
...
// MARK: GATT Profile
// Service UUID
let serviceUuid = CBUUID(string: "0000180c-0000-1000-8000-00805f9b34fb")
// Characteristic UUIDs
let readCharacteristicUuid = CBUUID(
string: "00002a56-0000-1000-8000-00805f9b34fb")
// Read Characteristic
var readCharacteristic:CBMutableCharacteristic!
...
/**
Build Gatt Profile.
This must be done after Bluetooth Radio has turned on
*/
func buildGattProfile() {
let service = CBMutableService(type: serviceUuid, primary: true)
let characteristicProperties = CBCharacteristicProperties.read
let characterisitcPermissions = CBAttributePermissions.readable
readCharacteristic = CBMutableCharacteristic(
type: readCharacteristicUuid,
properties: characteristicProperties,
value: nil, permissions: characterisitcPermissions)
service.characteristics = [ readCharacteristic ]
peripheralManager.add(service)
}
/**
Set a Characteristic to some text value
*/
func setCharacteristicValue(
_ characteristic: CBMutableCharacteristic,
value: Data
128
) {
characteristic.value = value
if central != nil {
peripheralManager.updateValue(
value,
for: readCharacteristic,
onSubscribedCentrals: [central])
}
}
...
/**
Connected Central requested to read from a Characteristic
*/
func peripheralManager(
_ peripheral: CBPeripheralManager,
didReceiveRead request: CBATTRequest)
{
let characteristic = request.characteristic
if let value = characteristic.value {
//let stringValue = String(data: value, encoding: .utf8)!
if request.offset > value.count {
peripheralManager.respond(
to: request, withResult: CBATTError.invalidOffset)
return
}
let range = Range(uncheckedBounds: (
lower: request.offset,
upper: value.count - request.offset))
request.value = value.subdata(in: range)
peripheral.respond(to: request, withResult: CBATTError.success)
}
delegate?.blePeripheral?(characteristicRead: request.characteristic)
}
...
129
Delegates
...
/**
Characteristic was read
- Parameters:
- characteristic: the Characteristic that was read
*/
@objc optional func blePeripheral(
characteristicRead fromCharacteristic: CBCharacteristic)
...
Controllers
...
@IBOutlet weak var characteristicValueTextField: UITextField!
...
/**
Generate a random String
- Parameters
130
- length: the length of the resulting string
/**
Set Read Characteristic to some random text value
*/
func setRandomCharacteristicValue() {
let stringValue = randomString(
length: Int(arc4random_uniform(
UInt32(blePeripheral.readCharacteristicLength - 1))
)
)
let value:Data = stringValue.data(using: .utf8)!
blePeripheral.setCharacteristicValue(
blePeripheral.readCharacteristic,
value: value
)
characteristicValueTextField.text = stringValue
}
...
/**
131
BlePeripheral statrted adertising
- Parameters:
- error: the error message, if any
*/
func blePerihperal(startedAdvertising error: Error?) {
if error != nil {
print("Problem starting advertising: " + error.debugDescription)
} else {
print("adertising started")
advertisingSwitch.setOn(true, animated: true)
setRandomCharacteristicValue()
randomTextTimer = Timer.scheduledTimer(
timeInterval: 5,
target: self,
selector: #selector(setRandomCharacteristicValue),
userInfo: nil,
repeats: true
)
}
}
...
When run, the App will be able to host a simple GATT profile with a single Characteris-
tic that sets the Characteristic to a random String every 5 seconds (Figure 7-5).
132
Figure 7-5. App screen showing random Characteristic value on Advertising Pe-
ripheral
Example code
The code for this chapter is available online
at: https://github.com/BluetoothLowEnergyIniOSSwift/Chapter07
133
Writing Data to a Peripheral
Data is sent from the Central to a Peripheral when the Central writes a value in a Char-
acteristic hosted on the Peripheral, presuming that Characteristic has write permis-
sions.
134
(properties & \
CBCharacteristicProperties.writeWithoutResponse.rawValue) != 0
Regardless of the initial data type, the value written to the Characetristic must be
sent as a Data object. Here is how to convert some common data types into a Data
object:
135
If the Characteristic supports write (with response), the peripheral didWriteValueFor
event gets triggered in the CBPeripheralDelegate following a write operation:
func peripheral(
_ peripheral: CBPeripheral,
didWriteValueFor characteristic: CBCharacteristic,
error: Error?)
{
}
Models
Modify the BlePeripheral class to include methods to test the write permissions of a
Characteristic, to write a value to a Characteristic, and to handle the resulting
CBPeripheralDelegate callback:
...
/**
Write a text value to the BlePeripheral
- Parameters:
- value: the value to write to the connected Characteristic
*/
func writeValue(value: String, to characteristic: CBCharacteristic) {
let byteValue = Array(value.utf8)
// cap the outbound value length to be
136
// less than the characteristic length
var length = byteValue.count
if length > characteristicLength {
length = characteristicLength
}
let transmissableValue = Data(Array(byteValue[0..<length]))
print(transmissableValue)
var writeType = CBCharacteristicWriteType.withResponse
if BlePeripheral.isCharacteristic(
isWriteableWithoutResponse: characteristic) {
writeType = CBCharacteristicWriteType.withoutResponse
}
peripheral.writeValue(
transmissableValue,
for: characteristic,
type: writeType)
print("write request sent")
}
/**
Check if Characteristic is writeable
- Parameters:
- characteristic: The Characteristic to test
137
return false
}
/**
Check if Characteristic is writeable with response
- Parameters:
- characteristic: The Characteristic to test
/**
Check if Characteristic is writeable without response
- Parameters:
- characteristic: The Characteristic to test
138
}
// MARK: CBPeripheralDelegate
/**
Value was written to the Characteristic
*/
func peripheral(
_ peripheral: CBPeripheral,
didWriteValueFor characteristic: CBCharacteristic,
error: Error?)
{
print("data written")
delegate?.blePeripheral?(
valueWritten: characteristic,
blePeripheral: self)
}
...
Delegates
...
/**
Value written to Characteristic
- Parameters:
- characteristic: the Characteristic that was written to
- blePeripheral: the BlePeripheral
*/
139
@objc optional func blePeripheral(
valueWritten characteristic: CBCharacteristic,
blePeripheral: BlePeripheral)
...
Storyboard
Create and link a UILabel in the GattTableViewCell to show the write property and a
UITextField and UIButton to allow a user to type and submit text to the Characteristic
(Figure 8-2):
140
Views
...
@IBOutlet weak var writeableLabel: UILabel!
...
func renderCharacteristic(characteristic: CBCharacteristic) {
uuidLabel.text = characteristic.uuid.uuidString
let isReadable = BlePeripheral.isCharacteristic(
isReadable: characteristic)
let isWriteable = BlePeripheral.isCharacteristic(
isWriteable: characteristic)
readableLabel.isHidden = !isReadable
writeableLabel.isHidden = !isWriteable
if isReadable || isWriteable {
noAccessLabel.isHidden = true
} else {
noAccessLabel.isHidden = false
}
}
...
Controllers
...
141
@IBOutlet weak var writeCharacteristicButton: UIButton!
@IBOutlet weak var writeCharacteristicText: UITextField!
...
/**
Load UI elements
*/
func loadUI() {
advertisedNameLabel.text = blePeripheral.advertisedName
identifierLabel.text = \
blePeripheral.peripheral.identifier.uuidString
characteristicUuidlabel.text = \
connectedCharacteristic.uuid.uuidString
readCharacteristicButton.isEnabled = true
// characteristic is not readable
if !BlePeripheral.isCharacteristic(
isReadable: connectedCharacteristic) {
readCharacteristicButton.isHidden = true
characteristicValueText.isHidden = true
}
// characteristic is not writeable
if !BlePeripheral.isCharacteristic(
isWriteable: connectedCharacteristic) {
writeCharacteristicText.isHidden = true
writeCharacteristicButton.isHidden = true
}
}
...
/**
User touched Read button. Request to write to the Characteristic
*/
@IBAction func onWriteCharacteristicButtonTouched(_ sender: UIButton) {
print("write button pressed")
writeCharacteristicButton.isEnabled = false
if let stringValue = writeCharacteristicText.text {
print(stringValue)
blePeripheral.writeValue(
142
value: stringValue,
to: connectedCharacteristic)
writeCharacteristicText.text = ""
}
}
...
/**
Characteristic was written to. Update UI
*/
func blePeripheral(
valueWritten characteristic: CBCharacteristic,
blePeripheral: BlePeripheral)
{
print("value written to characteristic!")
writeCharacteristicButton.isEnabled = true
}
...
Compile and run. The updated app will be able to scan for and connect to a Periph-
eral. Once connected, it lists services and characteristics, connect to Characteristics
and send and data (Figure 8-3).
143
Figure 8-3. App screens showing GATT Profile of connected Peripheral with
Write access to a Characteristic, and an interface to send text to a Characteris-
tic
// Characteristic is writable
144
var characteristicProperties = CBCharacteristicProperties.write
// Read support
characteristicProperties.formUnion(CBCharacteristicProperties.read)
Everything else about creating the Characteristic remains the same as for a readable
Characteristic:
145
ject. If the Characteristic has the CBCharacteristicProperties.write property, it is im-
portant to respond to the Central with a CBATTError status.
Value Description
More than one request may come in at a time, so it is useful to iterate through and
deal with each request separately.
func peripheralManager(
_ peripheral: CBPeripheralManager,
didReceiveWrite requests: [CBATTRequest])
{
for request in requests {
peripheral.respond(to: request, withResult: CBATTError.success)
// do something with the incoming data
}
}
146
Putting It All Together
Create a new project called WriteCharacteristic and copy the files from the previous
chapter's project.
Models
Modify the BlePeripheral class to create a writeable Characteristic and to handle in-
coming write requests:
...
// Service UUID
let serviceUuid = CBUUID(string: "0000180c-0000-1000-8000-00805f9b34fb")
// Characteristic UUIDs
let readWriteCharacteristicUuid = CBUUID(
string: "00002a56-0000-1000-8000-00805f9b34fb")
// Read Characteristic
var readWriteCharacteristic:CBMutableCharacteristic!
...
/**
Build Gatt Profile.
This must be done after Bluetooth Radio has turned on
*/
func buildGattProfile() {
let service = CBMutableService(type: serviceUuid, primary: true)
var characteristicProperties = CBCharacteristicProperties.read
characteristicProperties.formUnion(
CBCharacteristicProperties.notify)
var characterisitcPermissions = CBAttributePermissions.writeable
characterisitcPermissions.formUnion(CBAttributePermissions.readable)
readWriteCharacteristic = CBMutableCharacteristic(
147
type: readWriteCharacteristicUuid,
properties: characteristicProperties,
value: nil,
permissions: characterisitcPermissions)
service.characteristics = [ readWriteCharacteristic ]
peripheralManager.add(service)
}
...
/**
Connected Central requested to write to a Characteristic
*/
func peripheralManager(
_ peripheral: CBPeripheralManager,
didReceiveWrite requests: [CBATTRequest])
{
for request in requests {
peripheral.respond(to: request, withResult: CBATTError.success)
if let value = request.value {
delegate?.blePeripheral?(
valueWritten: value,
toCharacteristic: request.characteristic)
}
}
}
...
Delegates
...
/**
148
Value written to Characteristic
- Parameters:
- value: the Data value written to the Charactersitic
- characteristic: the Characteristic that was written to
*/
@objc optional func blePeripheral(
valueWritten value: Data,
toCharacteristic: CBCharacteristic)
...
Storyboard
Add a UILabel and UITextView to show the Characteristic Log in the UIView in the
Main.storyboard to create the App's user interface (Figure 8-4):
149
Figure 8-4. Project Storyboard
Controllers
Add a UITextView that logs the incoming Characteristic values, and a callback han-
dler for the the BlePeripheral writes:
...
@IBOutlet weak var characteristicLogText: UITextView!
...
/**
Value written to Characteristic
- Parameters:
150
- stringValue: the value read from the Charactersitic
- characteristic: the Characteristic that was written to
*/
func blePeripheral(
valueWritten value: Data,
toCharacteristic: CBCharacteristic)
{
//let stringValue = String(data: value, encoding: .utf8)
let hexValue = value.hexEncodedString()
characteristicLogText.text = characteristicLogText.text + \
"\n" + hexValue
if !characteristicLogText.text.isEmpty {
characteristicLogText.scrollRangeToVisible(NSMakeRange(0, 1))
}
}
...
Compile and run. The updated app will host a simple GATT Profile featuring a write-
only Characteristic.
Example code
The code for this chapter is available online
at: https://github.com/BluetoothLowEnergyIniOSSwift/Chapter08
151
Using Notifications
Being able to read from the Central has limited value if the Central does not know
when new data is available.
Notifications solve this problem. A Characteristic can issue a notification when it’s
value has changed. A Central that subscribes to these notifications will know when
the Characteristic’s value has changed, but not what that new value is. The Central
can then read the latest data from the Characteristic.
152
Programming the Central
Before the Central can subscribe to the Characteristic’s notifications, it is useful to
know if the Characteristic supports notifications. To determine if notifications are en-
abled, get the properties of the Characteristic and isolate the notify property.
// subscribe to a Characteristic
peripheral.setNotifyValue(true, for: characteristic)
When the Characterstic has been subscribed to or unsubscribed from, the peripheral
didUpdateNotificationStateFor callback is triggered in the CBPeripheralDelegate.
func peripheral(
_ peripheral: CBPeripheral,
didUpdateNotificationStateFor characteristic: CBCharacteristic,
error: Error?)
{
}
func peripheral(
_ peripheral: CBPeripheral,
didUpdateValueFor characteristic: CBCharacteristic,
153
error: Error?)
{
}
Models
...
/**
Subscribe to the connected characteristic.
/**
Unsubscribe from the connected characteristic.
154
self.peripheral.setNotifyValue(false, for: characteristic)
}
...
/**
Check if Characteristic is notifiable
- Parameters:
- characteristic: The Characteristic to test
// MARK: CBPeripheralDelegate
/**
Characteristic has been subscribed to or unsubscribed from
*/
func peripheral(
_ peripheral: CBPeripheral,
didUpdateNotificationStateFor characteristic: CBCharacteristic,
error: Error?)
{
print("Notification state updated for: " +
"\(characteristic.uuid.uuidString)")
print("New state: \(characteristic.isNotifying)")
delegate?.blePeripheral?(
subscriptionStateChanged: characteristic.isNotifying,
155
characteristic: characteristic,
blePeripheral: self)
if let errorValue = error {
print("error subscribing to notification: ")
print(errorValue.localizedDescription)
}
}
...
Delegates
...
/**
A subscription state has changed on a Characteristic
- Parameters:
- subscribed: true if subscribed, false if unsubscribed
- characteristic: the Characteristic that was subscribed/unsubscribed
- blePeripheral: the BlePeripheral
*/
@objc optional func blePeripheral(
subscriptionStateChanged subscribed: Bool,
characteristic: CBCharacteristic,
blePeripheral: BlePeripheral)
...
156
Storyboard
Create and link a UILabel in the GattTableViewCell to show the notification property
and a UILabel and UISwitch to show the Characteristic subscription state (Figure 9-
2):
Views
Modify the GattTableViewCell to hold a UILabel that shows the notification permis-
sions for a Characteristic, as well as functionality to show or hide the UILabel to re-
flect the Characteristic permissions:
157
Example 9-3. UI/Views/GattTableViewCell.swift
...
@IBOutlet weak var notifiableLabel: UILabel!
...
func renderCharacteristic(characteristic: CBCharacteristic) {
uuidLabel.text = characteristic.uuid.uuidString
print(characteristic.uuid.uuidString)
var isReadable = false
var isWriteable = false
var isNotifiable = false
if (characteristic.properties.rawValue & \
CBCharacteristicProperties.read.rawValue) != 0 {
print("readable")
isReadable = true
}
if (characteristic.properties.rawValue & \
CBCharacteristicProperties.write.rawValue) != 0 ||
(characteristic.properties.rawValue & \
CBCharacteristicProperties.writeWithoutResponse.rawValue) != 0 {
print("writable")
isWriteable = true
}
if (characteristic.properties.rawValue & \
CBCharacteristicProperties.notify.rawValue) != 0 {
print("notifiable")
isNotifiable = true
}
readableLabel.isHidden = !isReadable
writeableLabel.isHidden = !isWriteable
notifiableLabel.isHidden = !isNotifiable
if isReadable || isWriteable || isNotifiable {
noAccessLabel.isHidden = true
} else {
noAccessLabel.isHidden = false
}
158
}
...
Controllers
...
@IBOutlet weak var subscribeToNotificationLabel: UILabel!
@IBOutlet weak var subscribeToNotificationsSwitch: UISwitch!
...
func loadUI() {
advertisedNameLabel.text = blePeripheral.advertisedName
identifierLabel.text = \
blePeripheral.peripheral.identifier.uuidString
characteristicUuidlabel.text = \
connectedCharacteristic.uuid.uuidString
readCharacteristicButton.isEnabled = true
159
writeCharacteristicText.isHidden = true
writeCharacteristicButton.isHidden = true
}
}
...
/**
Characteristic subscription status changed. Update UI
*/
func blePeripheral(
subscriptionStateChanged subscribed: Bool,
160
characteristic: CBCharacteristic,
blePeripheral: BlePeripheral)
{
if characteristic.isNotifying {
subscribeToNotificationsSwitch.isOn = true
} else {
subscribeToNotificationsSwitch.isOn = false
}
subscribeToNotificationsSwitch.isEnabled = true
}
...
}
Compile and run. The app can scan for and connect to a Peripheral. Once con-
nected, it can display the GATT profile of the Peripheral. It can connect to Characteris-
tics, Subscribe to notifications, and receive data without polling.
When you hit the “Subscribe to Notifications" button, the text field should begin
populating with random text generated on the Peripheral (Figure 9-3).
161
Figure 9-3. Apps screen showing GATT Profile of connected Peripheral with Noti-
fications available in a Characteristic, and text read from a Characteristic
162
Creating the Characteristic
A Characteristic that supports notifications must have a
CBCharacteristicProperties.notify property:
// notification support
var characteristicProperties = CBCharacteristicProperties.notify
163
type: readWriteNotifyCharacteristicUuid,
properties: characteristicProperties,
value: nil,
permissions: characterisitcPermissions)
Responding to Callbacks
When a Central subscribes to a Characteristic, the peripheralManager didSub-
scribeTo method is triggered by the CBPeripheralManagerDelegate object. One of
the parameters is a CBCentral, the Central that is subscribed to the Characteristic. It
is important to save this reference to the Central as its a requirement in sending a no-
tification.
func peripheralManager(
_ peripheral: CBPeripheralManager,
central: CBCentral,
didSubscribeTo characteristic: CBCharacteristic)
{
// save a copy of the Central
self.central = central
}
func peripheralManager(
_ peripheral: CBPeripheralManager,
central: CBCentral,
didUnsubscribeFrom characteristic: CBCharacteristic)
{
// remove reference to the Central
self.central = null
}
164
Just prior to the Characteristic sending out a notification, the peripheralManagerIs-
Ready method is triggered:
func peripheralManagerIsReady(
toUpdateSubscribers peripheral: CBPeripheralManager)
{
}
Sending Notifications
Notifications are automatically sent when the CBPeripheralManager calls the update-
Value method:
peripheralManager.updateValue(
value,
for: readWriteNotifyCharacteristic,
onSubscribedCentrals: [central])
Models
Add a reference to the connected Central, build a GATT Profile with a notification sup-
port, and callback handlers to process Characteristic subscription and unsubscrip-
tion by a Central:
165
...
// MARK: GATT Profile
// Service UUID
let serviceUuid = CBUUID(string: "0000180c-0000-1000-8000-00805f9b34fb")
// Characteristic UUIDs
let readWriteNotifyCharacteristicUuid = CBUUID(
string: "00002a56-0000-1000-8000-00805f9b34fb")
// Read Characteristic
var readWriteNotifyCharacteristic:CBMutableCharacteristic!
// Connected Central
var central:CBCentral!
...
/**
Build Gatt Profile.
This must be done after Bluetooth Radio has turned on
*/
func buildGattProfile() {
let service = CBMutableService(type: serviceUuid, primary: true)
var characteristicProperties = CBCharacteristicProperties.read
characteristicProperties.formUnion(
CBCharacteristicProperties.notify)
var characterisitcPermissions = CBAttributePermissions.writeable
characterisitcPermissions.formUnion(CBAttributePermissions.readable)
readWriteNotifyCharacteristic = CBMutableCharacteristic(
type: readWriteNotifyCharacteristicUuid,
properties: characteristicProperties,
value: nil,
permissions: characterisitcPermissions)
service.characteristics = [ readWriteNotifyCharacteristic ]
peripheralManager.add(service)
randomTextTimer = Timer.scheduledTimer(
timeInterval: 5,
166
target: self,
selector: #selector(setRandomCharacteristicValue),
userInfo: nil,
repeats: true)
}
/**
Generate a random String
- Parameters
- length: the length of the resulting string
/**
Set Read Characteristic to some random text value
*/
func setRandomCharacteristicValue() {
let stringValue = randomString(
length: Int(arc4random_uniform(
UInt32(readCharacteristicLength - 1)))
)
167
let value:Data = stringValue.data(using: .utf8)!
readWriteNotifyCharacteristic.value = value
if central != nil {
peripheralManager.updateValue(
value,
for: readWriteNotifyCharacteristic,
onSubscribedCentrals: [central])
}
print("writing " + stringValue + " to characteristic")
}
...
/**
Connected Central subscribed to a Characteristic
*/
func peripheralManager(
_ peripheral: CBPeripheralManager,
central: CBCentral,
didSubscribeTo characteristic: CBCharacteristic)
{
self.central = central
delegate?.blePeripheral?(
subscriptionStateChangedForCharacteristic: characteristic,
subscribed: true)
}
/**
Connected Central unsubscribed from a Characteristic
*/
func peripheralManager(
_ peripheral: CBPeripheralManager,
central: CBCentral,
didUnsubscribeFrom characteristic: CBCharacteristic)
{
self.central = central
delegate?.blePeripheral?(
subscriptionStateChangedForCharacteristic: characteristic,
168
subscribed: false)
}
/**
Peripheral is about to notify subscribers of changes to a Characteris-
tic
*/
func peripheralManagerIsReady(
toUpdateSubscribers peripheral: CBPeripheralManager)
{
print("Peripheral about to update subscribers")
}
...
Delegates
...
/**
A subscription state has changed on a Characteristic
- Parameters:
- characteristic: the Characteristic that was subscribed/unsubscribed
- subscribed: true if subscribed, false if unsubscribed
*/
@objc optional func blePeripheral(
subscriptionStateChangedForCharacteristic \
characteristic: CBCharacteristic,
subscribed: Bool)
...
169
Storyboard
Add a UILabel and UISwitch to show the Notification state (Figure 9-4).
Add a UISwitch to show the subscribed state of the Characteristic, and a callback
handler to process the changes to the Characteristic subscription state.
...
@IBOutlet weak var subscribedSwitch: UISwitch!
...
/**
View disappeared. Stop advertising
*/
override func viewDidDisappear(_ animated: Bool) {
170
subscribedSwitch.setOn(false, animated: true)
advertisingSwitch.setOn(false, animated: true)
}
...
/**
A subscription state has changed on a Characteristic
- Parameters:
- characteristic: the Characteristic that was subscribed/unsubscribed
- subscribed: true if subscribed, false if unsubscribed
*/
func blePeripheral(
subscriptionStateChangedForCharacteristic: CBCharacteristic,
subscribed: Bool)
{
subscribedSwitch.setOn(subscribed, animated: true)
}
...
Compile and run. The app can handle subscriptions to a Characteristic and send noti-
fications when the Characteristic's value has changed (Figure 9-5).
171
Figure 9-5. App screen Advertising Peripheral with updates a Characteristic
Example code
The code for this chapter is available online
at: https://github.com/BluetoothLowEnergyIniOSSwift/Chapter09
172
Streaming Data
The maximum packet size you can send over Bluetooth Low Energy is 20 bytes.
More data can be sent by dividing a message into packets of 20 bytes or smaller,
and sending them one at a time
Bluetooth Low Energy transmits at 1 Mb/s. Between the data transmission time and
the time it may take for a Peripheral to process incoming data, there is a time delay
between when one packet is sent and when the next one is ready to be sent.
There are many ways to do this. One way is to set up a Characteristic with read,
write, and notify permissions, and to flag the Characteristic as “ready” after a write
has been processed by the Peripheral. This sends a notification to the Central, which
sends the next packet. That way, only one Characteristic is required for a single data
transmission.
173
Figure 10-1. The process of using notifications to handle flow control on a
multi-packed data transfer
Set up the parameters of the flow control and packet queue like this:
174
// Flow control response
let flowControlMessage = "ready"
The flow control works by requesting a Characteristic read event when a notification
callback is triggered:
func peripheral(
_ peripheral: CBPeripheral,
didUpdateValueFor characteristic: CBCharacteristic,
error: Error?)
{
print("characteristic updated")
if let value = characteristic.value {
if let stringValue = String(data: value, encoding: .ascii) {
if stringValue == flowControlMessage {
packetOffset += characteristicLength
if packetOffset < outboundByteArray.count {
writePartialValue(
value: outboundByteArray,
offset: packetOffset, to: characteristic)
} else {
// done writing message
}
}
}
}
}
175
The first packet of data is initialized and sent:
Subsequent packets are sent one at a time until there are no more left:
func writePartialValue(
value: [UInt8],
offset: Int,
to characteristic: CBCharacteristic)
{
// don't go past the total value size
var end = offset + characteristicLength
if end > outboundByteArray.count {
end = outboundByteArray.count
}
let transmissableValue = Data(Array(outboundByteArray[offset..<end]))
peripheral.writeValue(transmissableValue, for: characteristic)
}
176
Putting It All Together
Models
Add methods to the BlePeripheral to handle partial writes, properties to track flow
control, and modify the peripheral didUpdateValueFor method to handle the inboud
flow control value:
...
// MARK: Flow control
- Parameters:
- value: the value to write to the connected Characteristic
*/
func writeValue(value: String, to characteristic: CBCharacteristic) {
// get the characteristic length
let writeableValue = value + "\0"
packetOffset = 0
// get the data for the current offset
outboundByteArray = Array(writeableValue.utf8)
writePartialValue(
value: outboundByteArray,
offset: packetOffset,
177
to: characteristic)
}
/**
Write a partial value to the BlePeripheral
- Parameters:
- value: the full value to write to the connected Characteristic
- offset: the packet offset
*/
func writePartialValue(
value: [UInt8],
offset: Int,
to characteristic: CBCharacteristic)
{
// don't go past the total value size
var end = offset + characteristicLength
if end > outboundByteArray.count {
end = outboundByteArray.count
}
let transmissableValue = \
Data(Array(outboundByteArray[offset..<end]))
print("writing partial value: \(offset)-\(end)")
print(transmissableValue)
var writeType = CBCharacteristicWriteType.withResponse
if BlePeripheral.isCharacteristic(
isWriteableWithoutResponse: characteristic) {
writeType = CBCharacteristicWriteType.withoutResponse
}
peripheral.writeValue(
transmissableValue,
for: characteristic,
type: writeType)
print("write request sent")
}
...
178
/**
Value downloaded from Characteristic on connected Peripheral
*/
func peripheral(
_ peripheral: CBPeripheral,
didUpdateValueFor characteristic: CBCharacteristic,
error: Error?)
{
print("characteristic updated")
if let value = characteristic.value {
print(value.debugDescription)
print(value.description)
179
blePeripheral: self)
}
}
}
}
}
...
The resulting app can send larger amounts of data to a connected Peripheral by
queueing and transmitting packets one at a time (Figure 10-2).
180
Figure 10-2. App screens showing GATT Profile for the Advertising Peripheral
and multipart value queued to be sent to a Characteristic.
181
When the Peripheral's Characteristic is written to, the peripheralManager didReceive-
Write method is triggered in the CBPeripheralManagerDelegate object. Once the in-
coming data is processed, the flow control value can be written to the Characteristic
locally and the notification sent to the Central notifying it of the the change.
func peripheralManager(
_ peripheral: CBPeripheralManager,
didReceiveWrite requests: [CBATTRequest])
{
for request in requests {
// Do something with the incoming request.value
// notify the Central of a successful write
peripheral.respond(to: request, withResult: CBATTError.success)
// convert flow control into a Data object
let flowControlValue:Data = flowControlString.data(using: .utf8)!
// send flow control value
peripheralManager.updateValue(
flowControlValue,
for: readWriteNotifyCharacteristic,
onSubscribedCentrals: [request.central])
}
}
182
Putting It All Together
Copy the previous chapter's project into a new project.
Models
Add functionality to BlePeripheral to describe the flow control value, and to send the
flow control message when a write request is triggered:
...
// HARK: Flow Control
let flowControlString = "ready"
...
/**
Connected Central requested to write to a Characteristic
*/
func peripheralManager(
_ peripheral: CBPeripheralManager,
didReceiveWrite requests: [CBATTRequest])
{
for request in requests {
peripheral.respond(to: request, withResult: CBATTError.success)
// convert flow control into a Data object
let flowControlValue:Data = flowControlString.data(
using: .utf8)!
// send flow control value
peripheralManager.updateValue(
flowControlValue,
for: readWriteNotifyCharacteristic,
onSubscribedCentrals: [request.central])
if let value = request.value {
delegate?.blePeripheral?(
valueWritten: value,
183
toCharacteristic: request.characteristic)
}
}
}
...
The resulting app can receive a queued stream of data from a Central by issuing a
receive/response flow control on the Characteristic (Figure 10-3).
184
Figure 10-3. App screen showing multipart value queued to be sent to a Charac-
teristic.
Example code
The code for this chapter is available online
at: https://github.com/BluetoothLowEnergyIniOSSwift/Chapter10
185