Bluetooth Low Energy in iOS Swift
Bluetooth Low Energy in iOS Swift
Bluetooth Low Energy in iOS Swift
USING
Bluetooth Low Energy
IN IOS SWIFT
Tony Gaitatzis
2017
1st Edition
Tony Gaitatzis
BackupBrain Publishing, 2017
ISBN: 978-1-7751280-0-7
backupbrain.co
i
by Tony Gaitatzis
Copyright © 2015 All Rights Reserved
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 except 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 below.
[email protected]
This book contains code samples available under the MIT License, printed
below:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
I started with Bluetooth Low Energy in 2011 while making portable brain
imaging technology. Later, while working on a friend’s wearable electronics
startup, I ended up working behind teh scenes on the TV show America’s
Greatest Makers in the Spring of 2016.
Unlike other wireless technologies, BLE can be powered from a coin cell battery
for months at a time - perfect for a wearable or Internet of Things (IoT) project!
Because of its low power and short data transmissions, it is great for transmitting
bite size information, but not great for streaming data such as sound or video.
/**
This is a multiline comment
It features more than one line of comment
- Parameters:
- parameterOne: A description of what is expected for the first parameter */
func shortFunction() {
}
func longerFunction(value: String) {
}
func superLongFunctionName(
}
xi
Introduction
In this book you will learn the basics of how to program Central and Peripheral
devices that communicate over Bluetooth Low Energy using iOS in Swift. These
tutorials will culminate in three projects:
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 device.
Overview
Bluetooth Low Energy (BLE) is a digital radio protocol. Very simply, it works
by transmitting radio signals from one computer to another.
Bluetooth supports a hub-and-spoke model of connectivity. One device acts as a
hub, or “Central” in Bluetooth terminology. Other devices act as “Peripherals.”
The Central has two modes: scanning and connected. The Peripheral has two
modes: advertising and connected. The Peripheral must be advertising for the
Central to see it.
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 di
fferent 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
identifies it on the network. Peripherals advertise this MAC address along with
other information about the Peripheral’s settings.
Connecting
A Central may connect to a Peripheral after the Central has seen the Peripheral’s
advertisement. 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.
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
access. 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
Characteristics. This paradigm is called the Bluetooth Generic Attribute Profile
(GATT).
BLE uses UUIDs to label Services and Characteristics so that Services and
Characteristics can be identified accurately even when switching devices or
when several Characteristics share the same name.
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).
The antenna transmits and receives radio signals. The processor responds to
changes from the antenna and controls the antenna’s tuning, the advertisement
message, scanning, and data transmission of the BLE device.
Power and Range
BLE has 20x2 Mhz channels, with a maximum 10 mW transmission power, 20
byte packet size, and 1 Mbit/s speed.
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).
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 distance betweeen the Central and the Peripheral in meters (d) can be
approximated with this equation:
d ≈ 10 A − R10n
The radio propagation constant depends on the environment, but it is typically
somewhere 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:
d
≈
10
75 − 35 10 × 3.5
d
≈
10
40 35
d ≈ 14
Therefore the distance between the Peripheral and Central is approximately 14
meters.
Introducing iOS
iOS is an incredibly easy platform to program Bluetooth Low Energy.
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
programming. Xcode is a dream to work with, there are no developer registration
costs, and the Swift programming language is easy to use.
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 potential of this technology is amazing.
Xcode Setup
iPhones since versien 5 and and iPads since version 2 are designed to support
Bluetooth Low Energy.
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).
Figure 2-7. New XCode Project
The center panel is where code and storyboards are edited (Figure 2-8).
Figure 2-8. XCode code editor
Bootstrapping
The first thing to do in any software project is to become familiar with the
environment.
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
programmed. That allows you to report errors and status messages to the
computer when the programs are running (Figure 3-1).
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).
Figure 3-2.
Linked Framework List
Click the "+" button to add a new Framework. A dialog will pop up. Search for
"CoreBluetooth" in the search field (Figure 3-3):
Figure 3-3. Linked
Framework List
Click on "CoreBluetooth.framework" and click the "Add" button to add
Bluetooth support to a project (Figure 3-4).
Figure 3-4.
Linked Framework List
Enable Bluetooth
The user might turn the Bluetooth radio o ff any time. Therefore, every time the
App loads, it needs to check if check if Bluetooth is still enabled or has been
disabled, using 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
before it’s ready, the App must listen for the CentralManagerDelegate to respond
with a centralManagerDidUpdateState method, which is triggered by changes in
the Bluetooth radio status.
State Description
poweredOff Bluetooth is disabled
poweredOn Bluetooth is enabled
resetting Bluetooth radio is resetting
unautharized
App is not authorized to use Bluetooth
unknown There was a problem talking to the Bluetooth radio
unsupported Bluetooth is unavailable
Create a new project called Bootstrapping. Create packages and classes so that
the project structure resembles this:
Your code structure should now look like this (Figure 3-5).
Figure 3-5. Project Structure
Storyboard
/**
View loaded. Start Bluetooth radio. */
- Parameters:
- central: the reference to the central
*/
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" }
}
}
Import CoreBluetooth
Link the CoreBluetooth Framework from the project Settings (Figure 3-8).
_ 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")
}
}
...
}
State Description
poweredOff Bluetooth is disabled
poweredOn Bluetooth is enabled
resetting Bluetooth radio is resetting
unautharized
App is not authorized to use Bluetooth
unknown There was a problem talking to the Bluetooth radio
unsupported Bluetooth is unavailable
Create a new project called Bootstrapping with the following project structure
(Figure 3-9).
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
).
Example 3-2. Models/BlePeripheral.swift
import UIKit
import CoreBluetooth
class BlePeripheral : NSObject, CBPeripheralManagerDelegate { // MARK:
Peripheral properties
// Advertized name
let advertisingName = "MyDevice"
// MARK: Peripheral State
// 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 */
func peripheralManager(
{
print("restoring peripheral state")
}
/**
Bluetooth Radio state changed
*/
} }
Delegates
The BlePeripheralDelegate will relay important events from the BlePeripheral to
the ViewController (Example 3-3
).
Example 3-3. Delegates/BlePeripheralDelegate.swift
import UIKit
import CoreBluetooth
@objc protocol BlePeripheralDelegate : class { /**
Bluetooth Radio state changed
- Parameters:
- state: new CBManagerState */
Controllers
The main UIView will instantiate a BlePeripheral object and print a message to
the debugger when Bluetooth radio has turned on (Example 3-4
).
Example 3-4. UI/Controllers/ViewController.swift
import UIKit
import CoreBluetooth
class ViewController: UIViewController, BlePeripheralDelegate {
// MARK: BlePeripheral
// BlePeripheral
var blePeripheral:BlePeripheral!
/**
UIView loaded
*/
/**
Bluetooth radio state changed
- Parameters:
- state: the CBManagerState
*/
print("Bluetooth on")
case CBManagerState.poweredOff:
print("Bluetooth off")
default:
print("Bluetooth not ready yet...")
}
}
}
Bluetooth devices discover each other when they are tuned to the same radio
frequency, also known as a Channel. There are three channels dedicated to
device discovery in Bluetooth Low Energy (Table 4-1):
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 guaranteed to be on the same channel eventually.
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:
func startScan() {
scanCountdown = 5 // 5 seconds scanTimer = Timer.scheduledTimer(
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() }
}
func centralManager(
_ central: CBCentralManager,
didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi
RSSI: NSNumber)
{
let peripheralIdentifier = peripheral.identifier
}
For security reasons, iOS does not reveal the MAC address of a Peripheral.
Instead, it creates a 32-bit UUID identifier.
The Advertised name of a Peripheral is typically buried in the GAP
Advertisement data, which can be retrieved like this:
var advertisedName = advertisementData["kCBAdvDataLocalName"] as! String
Create a new project called Scanner, and copy everything from the previous
example.
Create a file structure that looks like this (Figure 4-3).
Figure
4-3. Project structure
This example will feature a UITableView to list discovered Peripherals
Models
Create a BlePeripheral class that holds information about a CBPeripheral.
Example 4-1. Models/BlePeripheral.swift
import UIKit
import CoreBluetooth
class BlePeripheral: NSObject {
// MARK: Peripheral properties
/**
Initialize BlePeripheral with a corresponding Peripheral
- Parameters:
- delegate: The BlePeripheralDelegate
- peripheral: The discovered Peripheral
*/
init(peripheral: CBPeripheral) { super.init()
self.peripheral = peripheral
Storyboard
Create a UITableView and connect it to the UINavigationController in place of
the default UIView. Give it the class name "PeripheralTableView."
// MARK: UI Elements
@IBOutlet weak var advertisedNameLabel: UILabel!
@IBOutlet weak var identifierLabel: UILabel!
@IBOutlet weak var rssiLabel: UILabel!
/**
Render Cell with Peripheral properties
*/
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.
// MARK: UI Elements
@IBOutlet weak var scanButton: UIButton!
// Default unknown advertisement name
let unknownAdvertisedName = "(UNMARKED)"
// PeripheralTableViewCell reuse identifier
let peripheralCellReusedentifier = "PeripheralTableViewCell"
/**
View loaded. Start Bluetooth radio.
*/
/**
User touched the "Scan/Stop" button
*/
stopBleScan()
} else {
startBleScan()
}
}
/**
Scan for Bluetooth peripherals
*/
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)
}
}
/**
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 */
func updateScanCounter() {
- 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))")
if blePeripheral.peripheral.identifier == \ peripheral.identifier
{
peripheralFound = true break
}
}
}
}
/**
/**
return number of sections. Only 1 is needed
*/
/**
Return number of Peripheral cells
*/
override func tableView(
_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int
{
return blePeripherals.count
}
/**
Return rendered Peripheral cell
*/
override func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
print("setting up table cell")
let cell = tableView.dequeueReusableCell(
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).
Figure 4-5. App screen prior to a Bluetooth scan and after discovering
Bluetooth Peripherals
Programming the Peripheral
The previous Chapter showed how to turn on the Bluetooth radio and detect if
Peripheral is supported by the iOS hardware
This chapter will show how to advertise a Bluetooth Low Energy Peripheral.
Advertising is simple. Create a Dictionary of advertising parameters and pass the
Dictionary into the CBPeripheralManager.startAdvertising method:
CBAdvertisementDataLocalNameKey: advertisingName ]
peripheralManager.startAdvertising(advertisementData)
{
if error != nil {
print ("Error advertising peripheral") print(error.debugDescription)
}
// store a copy of the updated Peripheral peripheralManager = peripheral
}
Create a new app called ExampleBlePeripheral, and copy everything from the
previous example.
This example will add a UISwitch that shows when a Peripheral has begun
Advertising.
Custom scanner callbacks will be created, which respond to events when
scanning has stopped.
Models
/**
Start Bluetooth Advertising.
*/
func startAdvertising() {
let advertisementData:[String: Any] = [
CBAdvertisementDataLocalNameKey: advertisingName ]
peripheralManager.startAdvertising(advertisementData)
}
// MARK: CBPeripheralManagerDelegate
/**
Peripheral started advertising
*/
{
if error != nil {
print ("Error advertising peripheral") print(error.debugDescription)
}
self.peripheralManager = peripheral
delegate?.blePerihperal?(startedAdvertising: error)
}
...
}
Delegates
Add a new method to the BlePeripheralDelegate to relay the Advertising started
event:
Example 4-5. Delegates/BlePeripheralDelegate.swift
... /**
BlePeripheral statrted adertising
- Parameters:
- error: the error message, if any
*/
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):
Figure 4-6. Project Storyboard
Controllers
Add a UILabel to display the Advertising name and a UISwitch to show the
Advertising 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 BlePeripheralDelegate method:
...
// MARK: UI Elements
@IBOutlet weak var advertisingLabel: UILabel! @IBOutlet weak var
advertisingSwitch: UISwitch!
...
/**
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 */
/**
View disappeared. Stop advertising
*/
- Parameters:
- error: the error message, if any
*/
} else {
print("adertising started")
advertisingSwitch.setOn(true, animated: true)
}
}
...
Compile and run the app. When it runs, a Bluetooth Peripheral will be
advertising (
Figure 4-7).
Figure 4-7. App screen showing advertising Peripheral
Example code
The code for this chapter is available online
at: https://github.com/BluetoothLowEnergyIniOSSwift/Chapter04
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 attached 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 Central 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 Peripheral (Figure 5-1).
func centralManager(
_ central: CBCentralManager, didConnect peripheral: CBPeripheral)
{
}
The didFailToConnect will be triggered otherwise.
func centralManager(
_ central: CBCentralManager,
didFailToConnect peripheral: CBPeripheral, error: Error?)
{
}
A disconnection can be initiated like this:
centralManager.cancelPeripheralConnection(peripheral)
When successful, the didDisconnectPeripheral event will be triggered:
func centralManager(
_ central: CBCentralManager,
didDisconnectPeripheral peripheral: CBPeripheral, error: Error?)
{
}
Figure 5-3.
Added project structure
Models
Modify the BlePeripheral to support CBPeripheralDelegate callbacks
Example 5-1. Models/BlePeripheral.swift
import UIKit
import CoreBluetooth
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 */
}
/**
Get a broadcast name from an advertisementData packet. This may be different
than the actual broadcast name */
/**
Determine if this peripheral is connectable
from it's advertisementData packet.
*/
/**
RSSI read from peripheral. */
func peripheral(
_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?)
{
print("RSSI: \(RSSI.stringValue)")
rssi = RSSI
delegate?.blePeripheral?(readRssi: rssi, blePeripheral: self)
} }
Delegates
Create a BlePeripheralDelegate class that lets the BlePeripheral trigger events as
a result of changes in its state.
Example 5-2. Models/BlePeripheralDelegate.swift
import UIKit
import CoreBluetooth
@objc protocol BlePeripheralDelegate: class { /**
RSSI was read for a Peripheral
- Parameters:
- rssi: the RSSI
- blePeripheral: the BlePeripheral
*/
@objc optional func blePeripheral( readRssi rssi: NSNumber, blePeripheral:
BlePeripheral)
Storyboard
{
stopBleScan()
let selectedRow = indexPath.row print("Row: \(selectedRow)")
print(blePeripherals[selectedRow])
}
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) { let
peripheralViewController = \
// 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
*/
}
/**
RSSI discovered. Update UI
*/
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) /**
*/
func centralManager(
_ central: CBCentralManager,
didDisconnectPeripheral peripheral: CBPeripheral, error: Error?)
{
print("Disconnected Peripheral: \(peripheral.name)") dismiss(animated: true,
completion: nil)
/**
Bluetooth radio state changed.
*/
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)
}
}
The resulting App will be one that can scan and connect to an advertising
Peripheral (
Figure 5-5).
Figure 5-5. App screen after connecting to a Peripheral
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
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
Characteristics.
Characterstic
Characterstic
Characterstic
Service/
Characterstic
Characterstic
Characterstic
Figure 6-1. GATT Profile filesystem metaphor
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
Central can write to this Characteristic, Peripheral will be notified when Write
the Characteristic value changes and Central will be notified when the
write operation has occurred.
Central can write to this Characteristic. Peripheral will be notified when Write
without Response the Characteristic value changes but the Central will not be
notified that
the write operation has occurred.
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
Characteristic. Therefore, when a Central uploads data to the Peripheral, the
Peripheral can “read” from the Characteristic. The Peripheral “writes” new data
to the Characteristic, and can “notify” the Central that the data is altered.
It is worth noting that Bluetooth Low Energy has a maximum data packet size of
20 bytes, with a 1 Mbit/s speed.
Programming the Central
The Central can be programmed to read the GATT Profile of the Peripheral after
connection, 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:
let serviceUuids = [ "1800", "1815" ] peripheral.discoverServices(serviceUuids)
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
CBPeripheralManagerDelegate.
There are Primary Services and Secondary services. Secondary Services are
contained within other Services; Primary Services are not. The type of Service
can be discovered by inspecting the CBService.isPrimary flag.
To discover the Characteristics hosted by these services, simply loop through the
discovered 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 {
Each Characteristic has certain permission properties that allow the Central to
read, write, or receive notifications from it (
Table 6-2).
Table 6-2. CBCharacteristicProperties
Value Permission Description
read Read Central can read data altered by the Peripheral
write Write Central can send data, Peripheral reads
writeWithoutResponse Write Central can send data. No response from
Peripheral
notify Notify Central is notified as a result of a change
In iOS, these properties are expressed as a binary integer which can be extracted
like this:
let properties = characteristic.properties.rawValue
let isWritable = (properties & \
CBCharacteristicProperties.write.rawValue) != 0;
let isWritableNoResponse = (properties & \
CBCharacteristicProperties.writeWithoutResponse.rawValue) != 0; let
isReadable = (properties & \
CBCharacteristicProperties.read.rawValue) != 0;
let isNotifiable = (properties & \
CBCharacteristicProperties.notify.rawValue) != 0;
A Note on Caching
This app will work like the one from the previous chapter, except that once the it
connects to the Peripheral, it will also list the GATT Profile for that Peripheral.
The GATT Profile will be displayed in an UITableView (Figure 6-2).
...
/**
Servicess were discovered on the connected Peripheral */
func peripheral(
_ peripheral: CBPeripheral,
didDiscoverServices error: Error?)
{
print("services discovered")
// clear GATT profile - start with fresh services listing gattProfile.removeAll()
if error != nil {
/**
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)")
}
delegate?.blePerihperal?(
discoveredCharacteristics: characteristics,
forService: service,
blePeripheral: self)
}
}
...
Delegates
Add a function to the BlePeripheralDelegate to alert when Characteristics have
been discovered:
Example 6-2. Delegates/BlePeripheralDelegate.swift
... /**
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)
Storyboard
}
}
Controllers
// 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!
/**
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
{ 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(
}
}
return cell
}
/**
Return the number of Service sections
*/
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)
}
// MARK: Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// 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?)
{
}
Value Description
readable The Characteristic’s Attributes can be read by a connected Central.
writeable The Characteristic’s Attributes can be altered by a connected Central.
Create Attribute permissions.
type: characteristicUuid,
properties: characteristicProperties, value: value,
permissions: characterisitcPermissions)
Add one or more Characteristics to a Service by creating a
[CBMutableCharacteristic] array.
// set the service Characterisic array service.characteristics = [ characteristic ]
For pedagogical reasons, many of the examples will not include this portion of
the GATT Profile.
Create a new project called GattProfile and copy everything from the previous
example.
Models
Modify BlePeripheral.swift to build a minimal Gatt Services profile. Build the
GATT Profile structure, and handle the callback when Services are added.
Example 6-5. Models/BlePeripheral.swift
... // 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)
}
...
/**
Peripheral added a new Service
*/
func peripheralManager(
_ peripheral: CBPeripheralManager, didAdd service: CBService,
error: Error?)
{
print("added service to peripheral") if error != nil {
print(error.debugDescription) }
}
/**
Bluetooth Radio state changed */
{
peripheralManager = peripheral
switch peripheral.state {
case CBManagerState.poweredOn:
buildGattProfile()
startAdvertising()
default: break
}
delegate?.blePeripheral?(stateChanged: peripheral.state) }
...
Once the Central has a Bluetooth GATT connection and has access to a
Characteristic 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
peripheral didUpdateValueFor method is triggered in the
CBPeripheralManagerDelegate.
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.
// convert to byte array
let byteArray = [UInt8](value)
// convert to String
let stringValue = String(data: value, encoding: .ascii)
// convert to signed integer
let intValue = value.withUnsafeBytes { (ptr: UnsafePointer<Int>) -> Int in
return ptr.pointee
}
// convert to float
let floatValue = value.withUnsafeBytes { (ptr: UnsafePointer<Float>) -> Float in
return ptr.pointee
}
CBCharacteristicProperties.read.rawValue) != 0 {
return true }
return false }
// 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 {
print(value.debugDescription) print(value.description)
characteristicRead: stringValue,
characteristic: characteristic,
blePeripheral: self)
}
}
}
}
Delegates
Add a method to the BlePeripheralDelegate that alerts subscribers of a read
operation on a Characteristic.
Example 7-2. Delegates/BlePeripheralDelegate.swift
... /**
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)
...
Storyboard
// MARK: UI Elements
@IBOutlet weak var uuidLabel: UILabel! @IBOutlet weak var readableLabel:
UILabel! @IBOutlet weak var noAccessLabel: UILabel!
/**
Render the cell with Characteristic properties
*/
noAccessLabel.isHidden = true
} else {
noAccessLabel.isHidden = false
}
}
}
Controllers
// 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!
super.viewDidLoad()
print("Will connect to device " + \
/**
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 */
/**
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")
}
}
/**
Bluetooth radio state changed
- Parameters:
- central: the reference to the central
*/
print("bluetooth on")
default:
print("bluetooth unavailable")
}
}
// MARK: - Navigation
/**
Animate the segue
*/
centralManager.cancelPeripheralConnection(
connectedBlePeripheral.peripheral)
}
}
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 */
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
connected, 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 Characteristic (Figure 7-4).
Figure 7-4. App screens showing GATT Profile for connected Peripheral
and values read from a Characteristic on a connected Peripheral
Programming the Peripheral
This chapter will show how to create a Characteristic with read access.
A read-only Characteristic is created and added to a Primary Service like this:
// create Characteristic UUID
let readCharacteristicUuid = CBUUID(
string: "00002a56-0000-1000-8000-00805f9b34fb")
When the Central requests to read data from the Peripheral’s Characteristic, the
peripheralManager didReceiveRead method is triggered the
CBPeripheralManagerDelegate 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,
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.
CBATTError status messages include the following:
Table 7-1. Common CBATTError values
Value Description
success Operation was successful
readNotPermitted Read request not permitted.
requestNotSupported Request was not supported.
invalidOffset Requested data offset is invalid.
For example, a successful read request warrents a CBATTError.success
response:
func peripheralManager(
_ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest)
{
let characteristic = request.characteristic
if let value = characteristic.value {
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)
peripheral.respond(to: request, withResult: CBATTError.success) }
}
Putting It All Together
Copy the previous chapter's project into a new project and modify the files
below.
Models
// 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
) { 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 {
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) }
...
Delegates
Add a method to the BlePeripheralDelegate that sends a notification when a
Characteristic has been read:
Example 7-7. Delegates/BlePeripheralDelegate.swift
... /**
Characteristic was read
- Parameters:
- characteristic: the Characteristic that was read */
Controllers
Add a method to the BlePeripheralDelegate that sends a notification when a
Characteristic has been read:
Example 7-7. UI/Controllers/ViewController.swift
/**
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 }
...
/** BlePeripheral statrted adertising
- Parameters:
- error: the error message, if any
*/
} 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
Characteristic that sets the Characteristic to a random String every 5 seconds (
Figure 7-5).
Figure 7-5. App screen showing random Characteristic value on
Advertising Peripheral
Example code
The code for this chapter is available online
at: https://github.com/BluetoothLowEnergyIniOSSwift/Chapter07
Writing Data to a Peripheral
Data is sent from the Central to a Peripheral when the Central writes a value in a
Characteristic hosted on the Peripheral, presuming that Characteristic has write
permissions.
CBCharacteristicProperties.write.rawValue) != 0 ||
(properties & \
CBCharacteristicProperties.writeWithoutResponse.rawValue) != 0
CBCharacteristicProperties.writeWithoutResponse.rawValue) != 0 The
Characteristic is written to like this:
peripheral.writeValue(value, for: characteristic)
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:
start: &intValue,
count: 1))
start: &floatValue,
count: 1))
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?)
{
}
Copy the project from the previous chapter into a new project, titled
"WriteCharacteristic."
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:
- Parameters:
- value: the value to write to the connected Characteristic */
length = characteristicLength
}
let transmissableValue = Data(Array(byteValue[0..<length]))
print(transmissableValue)
var writeType = CBCharacteristicWriteType.withResponse if
BlePeripheral.isCharacteristic(
/**
Check if Characteristic is writeable
- Parameters:
- characteristic: The Characteristic to test
- returns: True if characteristic is writeable */
static func isCharacteristic(
isWriteable characteristic: CBCharacteristic) -> Bool
{
if (characteristic.properties.rawValue & \
CBCharacteristicProperties.write.rawValue) != 0 ||
(characteristic.properties.rawValue & \
CBCharacteristicProperties.writeWithoutResponse.rawValue) != 0 { return true
}
return false }
/**
Check if Characteristic is writeable with response
- Parameters:
- characteristic: The Characteristic to test
- returns: True if characteristic is writeable with response
*/
static func isCharacteristic(
isWriteableWithResponse characteristic: CBCharacteristic) -> Bool { if
(characteristic.properties.rawValue & \
/**
Check if Characteristic is writeable without response
- Parameters:
- characteristic: The Characteristic to test
- returns: True if characteristic is writeable without response */
static func isCharacteristic(
isWriteableWithoutResponse characteristic: CBCharacteristic) -> Bool
{
if (characteristic.properties.rawValue & \
CBCharacteristicProperties.writeWithoutResponse.rawValue) != 0 { return true
}
return false
}
// 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
Add a function to the BlePeripheralDelegate to alert subscribers that a write
operation happened.
Example 8-2. Delegates/BlePeripheralDelegate.swift
... /**
Value written to Characteristic
- Parameters:
- characteristic: the Characteristic that was written to
- blePeripheral: the BlePeripheral
*/
@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):
Figure 8-2. Project Storyboard
Views
Modify the GattTableViewCell to display the appropriate UILabelView when a
Characteristic is writeable.
Example 8-3. UI/Views/GattTableViewCell.swift
Controllers
...
/**
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(
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
Peripheral. Once connected, it lists services and characteristics, connect to
Characteristics and send and data (Figure 8-3).
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
Characteristic
Programming the Peripheral
This chapter will show how to create a Characteristic with read access.
A writeable Characteristic is has the CBCharacteristicProperties.write property:
// Characteristic is writable
var characteristicProperties = CBCharacteristicProperties.write
It is possible to append other properties to additionally make the Characteristic
readable:
// Read support
characteristicProperties.formUnion(CBCharacteristicProperties.read)
// add write support
characteristicProperties.formUnion(CBCharacteristicProperties.notify)
Everything else about creating the Characteristic remains the same as for a
readable Characteristic:
// Characteristic Attributes are readable
var characterisitcPermissions = CBAttributePermissions.readable
// Set Characteristic UUID
let readWriteCharacteristicUuid = CBUUID( string: "00002a56-0000-1000-
8000-00805f9b34fb")
func peripheralManager(
_ peripheral: CBPeripheralManager, didReceiveWrite requests:
[CBATTRequest])
{
for request in requests {
peripheral.respond(to: request, withResult: CBATTError.success) // do
something with the incoming data
}
}
Create a new project called WriteCharacteristic and copy the files from the
previous chapter's project.
The Peripheral will be modified to support writes.
Models
Modify the BlePeripheral class to create a writeable Characteristic and to handle
incoming write requests:
Example 8-5. Models/BlePeripheral.swift
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(
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
Add a method to the BlePeripheralDelegate to process successful writes to a
Characteristic:
Example 8-6. Delegates/BlePeripheralDelegate.swift
... /** 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):
Figure 8-4. Project Storyboard
Controllers
Add a UITextView that logs the incoming Characteristic values, and a callback
handler for the the BlePeripheral writes:
Example 8-7. UI/Controllers/ViewController.swift
...
@IBOutlet weak var characteristicLogText: UITextView!
...
/**
Value written to Characteristic
- Parameters:
- 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
writeonly Characteristic.
Example code
The code for this chapter is available online
at: https://github.com/BluetoothLowEnergyIniOSSwift/Chapter08
Using Notifications
Being able to read from the Central has limited value if the Central does not
know when new data is available.
func peripheral(
_ peripheral: CBPeripheral,
didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?)
{
}
Once subscribed, any changes to a Characteristic automatically trigger the
peripheral didUpdateValueFor method in the CBPeripheralDelegate.
func peripheral(
_ peripheral: CBPeripheral,
didUpdateValueFor characteristic: CBCharacteristic, error: Error?)
{
}
Models
}
...
/**
Check if Characteristic is notifiable
- Parameters:
- characteristic: The Characteristic to test
- returns: True if characteristic is notifiable
*/
static func isCharacteristic(
isNotifiable characteristic: CBCharacteristic) -> Bool { if
(characteristic.properties.rawValue & \
CBCharacteristicProperties.notify.rawValue) != 0 {
return true }
return false }
// 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?(
}
}
...
Delegates
Add a method to the BlePeripheralDelegate to notify subscribers that a
Characteristic has been subscribed to or unsubscribed from:
Example 9-2. Delegates/BlePeripheralDelegate.swift
... /**
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)
...
Storyboard
Controllers
...
func loadUI() {
advertisedNameLabel.text = blePeripheral.advertisedName identifierLabel.text =
\
blePeripheral.peripheral.identifier.uuidString
characteristicUuidlabel.text = \
connectedCharacteristic.uuid.uuidString
readCharacteristicButton.isEnabled = true
Compile and run. The app can scan for and connect to a Peripheral. Once
connected, it can display the GATT profile of the Peripheral. It can connect to
Characteristics, 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).
Figure 9-3. Apps screen showing GATT Profile of connected Peripheral with
Notifications available in a Characteristic, and text read from a
Characteristic
Programming the Peripheral
Often, battery life is at a premium on Bluetooth Peripherals. For this reason, it is
useful to notify a connected Central when a Characteristic’s value has changed,
but not send the new data to the Central. Waking up the Bluetooth radio to send
one byte consumes less battery than sending 20 or more bytes.
func peripheralManager(
_ peripheral: CBPeripheralManager,
central: CBCentral,
didSubscribeTo characteristic: CBCharacteristic)
{
// save a copy of the Central self.central = central
}
When a Central unsubscribes from a Characterstic, peripheralManager
didUnsubscribeFrom method is triggered by the CBPeripheralManagerDelegate
object:
func peripheralManager(
_ peripheral: CBPeripheralManager,
central: CBCentral,
didUnsubscribeFrom characteristic: CBCharacteristic)
{
// remove reference to the Central self.central = null
}
Just prior to the Characteristic sending out a notification, the
peripheralManagerIsReady method is triggered:
func peripheralManagerIsReady(
toUpdateSubscribers peripheral: CBPeripheralManager)
{
}
Sending Notifications
Notifications are automatically sent when the CBPeripheralManager calls the
updateValue method:
peripheralManager.updateValue(
value,
for: readWriteNotifyCharacteristic, onSubscribedCentrals: [central])
// 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!
...
/**
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)
service.characteristics = [ readWriteNotifyCharacteristic ]
peripheralManager.add(service)
randomTextTimer = Timer.scheduledTimer(
timeInterval: 5,
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))) )
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?(
{
print("Peripheral about to update subscribers")
}
...
Delegates
Add a method to the BlePeripheralDelegate to relay changes in a Characteristic's
subscription state:
Example 9-6. Delegates/BlePeripheralDelegate.swift
... /**
A subscription state has changed on a Characteristic
- Parameters:
- characteristic: the Characteristic that was subscribed/unsubscribed
- subscribed: true if subscribed, false if unsubscribed
*/
subscribed: Bool)
...
Storyboard
Add a UILabel and UISwitch to show the Notification state (Figure 9-4).
Figure 9-4. Project Storyboard
Add a UISwitch to show the subscribed state of the Characteristic, and a
callback handler to process the changes to the Characteristic subscription state.
Example 9-7. UI/Controllers/VewController.swift
...
@IBOutlet weak var subscribedSwitch: UISwitch!
...
/**
View disappeared. Stop advertising
*/
override func viewDidDisappear(_ animated: Bool) {
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
notifications when the Characteristic's value has changed (
Figure 9-5).
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
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.
Set up the parameters of the flow control and packet queue like this:
// Flow control response
let flowControlMessage = "ready"
// outbound value to be sent to the Characteristic var outboundByteArray:
[UInt8]!
// packet offset in multi-packet value var packetOffset = 0
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 {
writePartialValue(
value: outboundByteArray,
offset: packetOffset, to: characteristic)
} else {
// done writing message
}
}
}
}
value: outboundByteArray,
offset: packetOffset,
to: characteristic)
}
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)
Models
...
/**
Write a text value to the BlePeripheral
- Parameters:
- value: the value to write to the connected Characteristic */
value: outboundByteArray,
offset: packetOffset,
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 = \
print(stringValue)
// received response from Peripheral
delegate?.blePeripheral?(
characteristicRead: stringValue,
characteristic: characteristic,
blePeripheral: self)
if stringValue == flowControlMessage {
packetOffset += characteristicLength
if packetOffset < outboundByteArray.count {
writePartialValue(
value: outboundByteArray,
offset: packetOffset, to: characteristic)
} else {
print("value write complete")
// done writing message
delegate?.blePeripheral?(
valueWritten: characteristic,
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).
Figure 10-2. App screens showing GATT Profile for the Advertising
Peripheral and multipart value queued to be sent to a Characteristic.
Programming the Peripheral
The Peripheral in this example processes a value written to a Characteristic, then
sets the Characteristic's value to the "ready" flow control message, and sends a
notification to the Central. In this way, the Central is triggered to read the
Characteristic. When the Central reads the flow control message, it knows when
to send the next packet of data.
When the Peripheral's Characteristic is written to, the peripheralManager
didReceiveWrite method is triggered in the CBPeripheralManagerDelegate
object. Once the incoming 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])
}
}
...
/**
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])
Due to the nature of how radio signals diminish in intensity with distance,
Bluetooth Peripherals can be used both for range finding and spacial awareness.
Range Finding
Bluetooth signals can be used to approximate the distance between a Peripheral
and a Central because the radio signal quality drops off in a predictable way with
distance. The diagram below shows how the signal might drop as distance
increases (Figure 11-1).
One or more Centrals can approximate their distance from a single iBeacon
without connecting.
Spacial Awareness
A Central can approximate its position in space using a process called
trilateration. Trilateration works by a computing series of equations when both
the distance from and location of nearby iBeacons are known (Figure 11-2).
Figure 11-2. Example Central and iBeacon positions in a room
This is a pretty math-intensive process, but it’s all based on the Pythagorus
Theorem. By calculating the shape of the triangles made from the relative
positions of all the iBeacons and the Central, one can determine the location of
the Central (iBeacons and the Central, one can determine the location of the
Central ( 3).
Figure 11-3. Distances from iBeacons to Central
iBeacons
The Scan Result allows a Central to read information from a Peripheral without
connecting to it, in much the same way that the advertising name is read.
iBeacons are beacons that advertise information about their location and
advertise intensity using the Scan Result feature of Bluetooth Low Energy.
The Scan Result allows a Central to read information from a Peripheral without
connecting to it, in much the same way that the Device name is advertised.
There are only two tricks to creating an iBeacon client in iOS:
1. All iBeacons for the same location service have the same UUID. 2. iBeacons
have Major and Minor numbers for a sort of layered identification. What the
Major and Minor numbers represent are dependant on the iBeacon administrator,
but typically a Major number represents iBeacons for a particular organization or
building, and a Minor number represents iBeacons from a specific area or floor
of a building, etc.
The museum's smartphone app has an internal data set relating Major and Minor
values to the floors and rooms of the exhibit. It scans for all iBeacons with a
specific UUID. The nearby iBeacons are discovered and read. The discovered
iBeacons' Major and Minor numbers are looked up to learn that the user is in a
specific room on a specific floor in the museum. Relevant content is accessed
from the museum's API and loaded in the smartphone app.
Programming the Central
There are two parts to working with iBeacon on iOS:
1. Requesting Access to the CoreLocation Framework 2. Searching for iBeacons
Before doing anything, the user must authorized the device to use the
CoreLocation library. This is done by checking the current
CLAuthorizationStatus, which states if the app is allowed to access the
CoreLocation Framework:
Value Description
notDetermined
restricted
denied
authorizedAlways
authorizedWhenInUse
The user has not yet made a choice regarding whether this app can use location
services.
This app is not authorized to use location services due to active restrictions.
The user explicitly denied the use of location services for this app or location
services are currently disabled in Settings.
This app is authorized to start location services at any time, including monitoring
for location changes in the background.
This app is authorized to start most location services while running in the
foreground, but not when in the background.
The new CLAuthorazitionStatus will come back when the locationManager
didChangeAuthorization callback is triggered by the
CLLocationManagerDelegate.
func locationManager(
_ manager: CLLocationManager,
didChangeAuthorization status: CLAuthorizationStatus)
{
if status == .authorizedAlways {
// location manager is authorized all the time for this app. proceed } else if status
== .authorizedWhenInUse){
// location manager is authorized when the app is // in the foreground. proceed
} else {
// location manager is unauthorized
}
}
// only look for specific iBeacon UUIDs (known as a Proximity UUID) let
proximityUuid = UUID(uuidString: "E20A39F4-73F5-4BC4-A12F-
17D1AD07A961") // build one or more iBeacon "region" to search for
// using the Proximity UUID. Each has a unique identifier for reuse later let
beaconRegion = CLBeaconRegion(proximityUUID: UUID(
uuidString: proximityUuid!,
identifier: "regionUniqueId")
iBeacons may contain Major or Minor numbers as well, which further identify
groups of iBeacons. It is possible to create an iBeacon Region that isolates both
the Proximity UUID and the Major number:
Or a Region that isolates the UUID, Major number, and Minor number:
let beaconRegion = CLBeaconRegion( proximityUUID: proximityUuid, major:
1122,
minor: 3344,
identifier: "anotherRegionUniqueId")
This method will return a CLBeacon array periodically. This array may have
zero or more elements, representing all the iBeacons it found during the Ranging
process. This may include iBeacons that it found during the previous Ranging
scan, so watch out for duplicates.
func locationManager(
_ manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], in
region: CLBeaconRegion)
{
//beaconswillcontainanarrayof0ormoreCLBeacons
//EachscanmaycontainCLBeaconsthatwerecontainedinthe
//lasttimelocationManagerdidRangeBeaconswastriggered
}
The iBeacon major number and minor number can be retrieved from the
CLBeacon object:
return "Unknown"
case .immediate:
return "Immediate"
case .near:
return "Near"
case .far:
return "Far"
}
}
If the transmission power of the iBeacon is known, that can be useful for
determining how far away the iBeacon is.
Approximate the distance from a Central to an iBeacon using the RSSI reported
at that distance, a reference RSSI at 1 meter, and an approximate propagation
constant, using this equation:
d
≈
10
A − R 10n
func getDistanceFromRssi(
rssi: Int, referenceRssi: Int, propagationConstant: Float) -> Double
{
letexponent=Double(referenceRssi-rssi)/(10*propagationConstant);
letdistance=pow(10.0,exponent)
returndistance
Due to radio interference and absorption from surrounding items, the radio
propagation constant, n, varies a lot from environment to environment and even
step to step. This makes it very difficult to know the exact distance between a
Central and an iBeacon. The radio propagation constant can be approximated by
testing the iBeacon’s RSSI at 1 meter in conditions similar to what is expected in
the field.
Spacial Awareness
Imagine a room with known iBeacon’s locations, P1, P2, and P3 as x,y
coordinates (Figure 11-5).
Figure 11-5. Example iBeacon positions in a room
// three iBeacons with known distances from a fixed spot let p1 = new
CGPoint(x: 10, y: 10)
let p2 = new CGPoint(x: 50, y: 30)
let p3 = new CGPoint(x: 35, y: 50)
Using the getDistanceFromRssi method, the distance between each iBeacon and
the Central has been derived as r1, r2, r3 (
Figure 11-6).
Figure 11-6. Beacon distances from Central derived from their singal
strengths
// distance to each of the iBeacons has already been calculated let r1 = 13.2,
let r2 = 10.8,
let r3 = 7.7;
=
P2 − P1
P2 − P1
e
x
=
P2
(
d
,
P2y − P1yx − P1x
d )
let exx = (p2.x - p1.x) / d let exy = (p2.y - p1.y) / d
Find the magnitude i of the distance between P1 and P3 in the x direction (Find
the magnitude i of the distance between P1 and P3 in the x direction ( 9).
Figure 11-9. Derived horizontal component of distance from Beacon 1 to
Beacon 2
i = ex ⋅ (P3 − P1)
i = exx(P3x − P1x) +exy(P3y − P1y)
let i = exx * (p3.x - p1.x) + exy * (p3.y - p1.y)
Calculate the unit vector êy between P1 and P3, showing the direction between
P1 and P3 (Figure 11-10).
Figure 11-10. Derived direction from iBeacon 1 and iBeacon 3
e
y
=
P3 − P1 − i ̂x
P
3
−
P
1
−
i
ex
e
y
=
P3x − P1x − iexx , y − P1y − iexyP3
2 2 2 2
(P3x − P1x − iexx) +(P3y − P1y − iexy) (P3x − P1x − iexx) +(P3y − P1y − iexy)
let eyx = (p3.x - p1.x - i * exx) / sqrt( pow(p3.x - p1.x - i * exx, 2) +
pow(p3.y - p1.y - i * exy, 2)
)
let eyy = sqrt(
pow(p3.x - p1.x - i * exx, 2) +
Use the relative distances from the Central to iBeacons P1, P2, and P3 to find
relative distances x and y from each iBeacon (Figure 11-12).
Figure 11-12. Derived positions of each iBeacon with horizontal position of
Central
This is done by solving for x and y in the following equations:
x
=
r2 − r2 + d21 2
2d
y
=
r2 − r2 + i2 + j2 i1 3
−
2j j x
let x = (pow(Double(r1), 2) - pow(Double(r2), 2) + pow(d, 2) ) / (2 * d) let y =
(pow(Double(r1), 2) - pow(Double(r3), 2) + pow(i, 2) + \ pow(j, 2)) / (2 * j) - i *
x / j
Finally, find the position of the Central at P4 by adding the distance between P4
and P1 to the relative position of P1 (Figure 11-13
).
Figure 11-13. Derived Central position
P
4 =
P
1 +
x
x + y ̂
e ey
P
4 =
(
P
1
x
+
x
e
x
x
+
y
e
y
x
,
P
1
y
+
x
e
x
y
+ y e ̂
yy)
That is how to trilaterate the position of a Central from three known iBeacon
positions.
The following code will create an App that graphs the position of nearby
iBeacons, lists their properties, and attempts to find its own location relative to
the iBeacons.
Create a new app called BeaconLocator. Create the following package structure
and copy files from the previous examples where they exist.
The final file structure should resemble this (Figure 11-14).
Figure 11-14. Project Structure
Frameworks
Import the CoreLocation Framework (Figure 11-15).
Figure 11-15.
CoreLocation Framework linked into project
Import the CoreLocation APIsin the code header:
import CoreLocation
Models
The iBeacon class encapsulates a known iBeacon type and allows for the
insertion of the physical location and transmission power of those iBeacons.
Example 11-1. Models/iBeacon.swift
import UIKit
import CoreLocation
class IBeacon: NSObject {
static let uuid = UUID(
uuidString: "e20a39f4-73f5-4bc4-a12f-17d1ad07a961")
return beacon.proximity.rawValue }
/**
Compare beacons
- Parameters:
- to: An IBeacon to compare
- Returns: true if same beacon, false otherwise.
*/
func isEqual(to: IBeacon) -> Bool {
if let thisBeacon = beacon {
}
}
return false
}
The CentralLocator trilaterates the user's device from known iBeacon locations.
Example 11-2. Models/CentralLocator.swift
import UIKit
class CentralLocator: NSObject {
/**
Trilaterates a Central position from three known Beacon positions */
(p3.yPosition_m - p1.yPosition_m)
//the unit vector in the y direction.
let eyx = (p3.xPosition_m - p1.xPosition_m - i * exx) / sqrt(
// result coordinates
let finalX = p1.xPosition_m + x * exx + y * eyx let finalY = p1.yPosition_m + x
* exy + y * eyy
Storyboard
// MARK: UI Elements
@IBOutlet weak var rssiLabel: UILabel! @IBOutlet weak var xPositionLabel:
UILabel! @IBOutlet weak var yPositionLabel: UILabel! @IBOutlet weak var
majorNumberLabel: UILabel! @IBOutlet weak var minorNumberLabel:
UILabel! @IBOutlet weak var proximityLabel: UILabel! @IBOutlet weak var
accuracyLabel: UILabel!
/**
Render the cell with Beacon properties
*/
func renderBeacon(_ iBeacon: IBeacon) {
majorNumberLabel.text = iBeacon.beacon.major.stringValue
minorNumberLabel.text = iBeacon.beacon.minor.stringValue rssiLabel.text =
String(iBeacon.beacon.rssi)
proximityLabel.text = String(iBeacon.beacon.proximity.rawValue)
accuracyLabel.text = String(iBeacon.beacon.accuracy)
Controllers
Create an iBeaconMapLayout class that displays iBeacon and Central positions
on the screen.
Example 11-4. UI/Controllers/GameViewController.swift
import UIKit
import SpriteKit import GameplayKit import CoreLocation
// MARK: UI Elements
@IBOutlet weak var beaconTableView: UITableView! // Beacon Map Scene
var scene:BleMapScene!
// Beacon TableView Cell Reuse Identifier
let beaconCellReuseIdentifier = "BeaconTableViewCell"
proximityUUID: IBeacon.uuid!,
identifier: "beaconRange")
/**
View Loaded
*/
skView.isMultipleTouchEnabled = true
scene = BleMapScene(size: skView.bounds.size) scene.scaleMode = .aspectFill
skView.presentScene(scene)
}
// set up Location Manager
locationManager.delegate = self;
// If location services are not enabled, request authorization if
(CLLocationManager.authorizationStatus() != \
CLAuthorizationStatus.authorizedWhenInUse) {
locationManager.requestWhenInUseAuthorization()
} else {
startScanning()
}
}
/**
Start scanning for Beacons
*/
func startScanning() {
print("starting scan")
locationManager.startMonitoring(for: beaconRegion)
locationManager.startRangingBeacons(in: beaconRegion)
}
/**
Map the Beacons and Central if possible. */
func mapBeacons() {
for beacon in foundBeacons {
mapBeacon(beacon)
}
if foundBeacons.count > 3 {
}
}
/**
Put a Beacon on the map
*/
/**
Put the Central on the map
*/
func mapCentralPosition() {
centralPosition = CentralLocator.trilaterate(foundBeacons) if let centralPosition
= centralPosition {
print("mapping location")
if let scene = scene {
scene.addCentral(position: centralPosition) } }
}
// MARK: CLLocationManagerDelegate callbacks
/**
Beacons discovered by Location Manager */
func locationManager(
_ manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], in
region: CLBeaconRegion)
{
// inspect previously discovered beacons
print(beacons.count)
for beacon in beacons {
}
}
if !beaconFound {
foundBeacons.append(iBeacon)
}
}
mapBeacons()
beaconTableView.reloadData()
/**
*/
func locationManager(
_ manager: CLLocationManager,
didChangeAuthorization status: CLAuthorizationStatus)
{
if (status == .authorizedAlways) ||
for: CLBeaconRegion.self) {
if CLLocationManager.isRangingAvailable() { startScanning()
} else {
print("ranging unavailable")
}
}
} else {
print("location manager unauthorized")
}
}
// MARK: UITableViewDataSource
/**
Number of Table sections
*/
/**
Number of iBeacons in table */
func tableView(
_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int
{
return foundBeacons.count
}
/**
Render Beacon Table cell
*/
func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
// create a new Beacon Table View cell let cell =
tableView.dequeueReusableCell(
/**
User selected a cell
*/
{
tableView.deselectRow(at: indexPath, animated: true)
}
Scenes
Create a BeaconMapScene, which will plot the iBeacon and Central locations on
a map.
Example 11-5. Scenes/BeaconMapScene.swift
import SpriteKit import GameplayKit
class BleMapScene: SKScene {
// sprite scale
let iconScale = CGFloat(0.1)
// size of room in meters - must same x/y ratio as screen bounds let roomBounds
= CGSize(width: 50, height: 37)
/**
Initialize Map Scene with a coder (required but not implemented) */
/**
Initialize Map Scene
*/
}
/**
Update scene
*/
override func update(_ currentTime: TimeInterval) { super.update(currentTime)
}
/**
Add Central to map
- Parameters:
- position: the x,y location of the central in meters from origin
*/
func addCentral(position: CGPoint) {
// load sprite
let central = SKSpriteNode(imageNamed: "central")
// scale and position sprite
central.size = CGSize(
width: central.size.width * iconScale,
height: central.size.height * iconScale)
central.position = getMapPosition(position)
// add sprite to map
addChild(central)
}
/**
Add a Beacon to the Map
- Parameters:
- position: the x,y location of the Beacon in meters from origin
- radius: the beacon's distance from the central in meters
*/
func addBeacon(position: CGPoint, radius: CGFloat) {
// load sprite
let peripheral = SKSpriteNode(imageNamed: "peripheral") // scale sprite
peripheral.size = CGSize(
/**
Convert Real world coordinates into map coordinates */
/**
Convert real world distances into map distances */
The resulting app will be able to locate iBeacons and, if there are three or more
iBeacons nearby, it can approximate its own location (
Figure 11-17).
Figure 11-17. App screen showing derived iBeacon and Central positions
Programming the Peripheral
An iBeacon is relatively simple to implement in iOS using the CoreLocation and
CoreBluetooth Frameworks.
Link the CoreBluetooth and CoreLocation Frameworks from the project Settings
(Figure 11-18).
Import the CoreBluetooth and CoreLocation APis in the code header to access
both the Bluetooth and iBeacon APIs. The working class must also implement
CBPeripheralManagerDelegate:
Import CoreBluetooth
import CoreLocation
Implementation
startAdvertising()
case CBManagerState.poweredOff:
stopAdvertising()
default: break
}
delegate?.iBeaconPeripheral?(stateChanged: peripheral.state) }
Each iBeacon uses a UUID plus Major and Minor numbers for identification.
iBeacons belonging to the same organizational unit also share the same UUID.
For instance all iBeacons belonging to a grocery store chain.
// UUID
let beaconUuid = UUID(uuidString: "e20a39f4-73f5-4bc4-a12f-17d1ad07a961")
Major numbers are typically used to identify iBeacons sharing the same purpose
typically have the same Major number, ones that identify each grocery store in
that chain.
// Major Number
let majorNumber:CLBeaconMajorValue = UInt16(1122)
The Minor numbers are typically unique between Major numbers, so that a user
can track their movement around the grocery store using the iBeacon minor
numbers for identification.
// Minor Number
let minorNumber:CLBeaconMinorValue = UInt16(3344)
The transmission power is important in both in setting the range of the iBeacon
and in allowing the Central to determine how close it is to an iBeacon
// Transmission power
let transmissionPower_db:NSNumber? = -56
Create an iBeacon region:
// Range identifier
let rangeIdentifier = "customUniqueIdentifier"
let beaconRegion = CLBeaconRegion( proximityUUID: beaconUuid!, major:
majorNumber,
minor: minorNumber,
identifier: rangeIdentifier)
Based on the iBeacon Region created earlier, create the Advertisement Data that
includes the iBeacon UUID, Major Number, Minor Number, and transmission
power:
Define the callback function that's called when the iBeacon begins advertising:
{
if error != nil {
print ("Error advertising peripheral")
print(error.debugDescription)
}
self.peripheralManager = peripheral
delegate?.iBeaconPeripheral?(startedAdvertising: error)
}
Stop advertising when the user closes the App:
Figure 11-18.
Project Structure
Frameworks
Import the CoreBluetooth and CoreLocation Frameworks (Figure 11-19):
Figure 11-19.
CoreBluetooth and CoreLocation Frameworks linked into project
Import the CoreBluetooth and CoreLocation APIs in the code header:
import CoreBluetooth import CoreLocation
Models
Create an iBeaconPeripheral class that has a Major Number, Minor Number, and
UUID. It will begin advertising as soon as the Bluetooth radio turns on:
Example 11-6. Models/iBeaconPeripheral.swift
import UIKit
import CoreBluetooth import CoreLocation
// Advertized name
let advertisingName = "LedRemote"
// Device identifier
let beaconIdentifier = "com.example.ibeacon" // UUID
let beaconUuid = UUID(
// Beacon Region
var beaconRegion: CLBeaconRegion!
// Peripheral Manager
var peripheralManager:CBPeripheralManager! // Connected Central
var central:CBCentral!
// delegate
var delegate:IBeaconPeripheralDelegate!
/**
Initialize BlePeripheral with a corresponding Peripheral
- Parameters:
- delegate: The BlePeripheralDelegate
- peripheral: The discovered Peripheral
*/
init(delegate: IBeaconPeripheralDelegate?) { super.init()
self.delegate = delegate
print("initializing beacon region") if let beaconUuid = beaconUuid {
beaconRegion = CLBeaconRegion(
proximityUUID: beaconUuid, major: majorNumber,
minor: minorNumber,
identifier: beaconIdentifier)
/**
Stop advertising, shut down the Peripheral */
func stopAdvertising() {
peripheralManager.stopAdvertising()
delegate?.iBeaconPeripheralStoppedAdvertising?()
}
/**
Start Bluetooth Advertising. This must be after building the GATT profile
*/
func startAdvertising() {
print("loading peripheral data")
let advertisementDictionary = beaconRegion.peripheralData(
withMeasuredPower: transmissionPower_db)
print("building advertisement data")
var advertisementData = [String: Any]()
for (key, value) in advertisementDictionary {
advertisementData as [String:Any])
}
// MARK: CBPeripheralManagerDelegate
/**
Peripheral will become active
*/
func peripheralManager(
_ peripheral: CBPeripheralManager, willRestoreState dict: [String : Any])
/**
Peripheral started advertising
*/
{
if error != nil {
print ("Error advertising peripheral")
print(error.debugDescription)
}
self.peripheralManager = peripheral
delegate?.iBeaconPeripheral?(startedAdvertising: error)
/**
Bluetooth Radio state changed
*/
Delegates
Create an iBeaconPeripheralDelegate class that relays iBeacon state changes:
Example 11-7. Delegates/iBeaconPeripheralDelegate.swift
import UIKit
import CoreBluetooth
@objc protocol IBeaconPeripheralDelegate : class {
/**
iBeacon State Changed
- Parameters:
- rssi: the RSSI
- blePeripheral: the BlePeripheral
*/
@objc optional func iBeaconPeripheral( stateChanged state: CBManagerState)
/**
iBeacon statrted advertising
- Parameters:
- error: the error message, if any
*/
/**
iBeacon stopped advertising
*/
@objc optional func iBeaconPeripheralStoppedAdvertising()
Storyboard
Create the UISwitches, UITextViews, and UILabels in the UIView in the
Main.storyboard to create the App's user interface (Figure 11-20).
Figure 11-20. Project Storyboard
Controllers
The ViewController instantiates the iBeacon, starts Advertising as soon as the
Bluetooth radio turns on, and shows the Advertising state in a UISwitch :
Example 11-8. UI/Controllers/ViewController.swift
import UIKit
import CoreBluetooth
import AVFoundation
class ViewController: UIViewController, IBeaconPeripheralDelegate {
// MARK: UI Elements
@IBOutlet weak var advertisingSwitch: UISwitch!
// MARK: BlePeripheral
// BlePeripheral
var iBeacon:IBeaconPeripheral!
/**
UIView loaded
*/
override func viewDidLoad() { super.viewDidLoad()
}
/**
View appeared. Start the Peripheral
*/
/**
View will appear. Stop transmitting random data */
/**
View disappeared. Stop advertising
*/
- Parameters:
- state: the CBManagerState representing the new state */
print("Bluetooth on")
case CBManagerState.poweredOff:
print("Bluetooth off")
default:
print("Bluetooth not ready yet...")
}
}
/**
RemoteLed statrted adertising
- Parameters:
- error: the error message, if any
*/
} else {
print("adertising started")
advertisingSwitch.setOn(true, animated: true)
}
}
/**
RemoteLed statrted adertising
*/
And yet it must support all the features you’ve learned so far in this book -
advertising, reads, writes, notifications, segmented data transfer, and encryption.
It’s a sophisticated program!
The Echo Server sends and receives text. Text is complicated because computers
communicate using binary data, not text. As a result, there are a couple things
that need to be done to protect the data from errors.
Data Formatting
One easy way to do this with text is to append a newline character to the end of
the outbound transmission.
let partialMessage = "Hello World" let message = partialMessage + "\n"
When the message is broken apart for transmission, the parts are reassembled
properly and the new line at the end separates new messages into new lines.
Everything else works the same way as any other Bluetooth app.
GATT Profile
The GATT Profile will be set up as a digital input/output under the Automation
IO (0x1815) Service, witch commands to and responses from the Peripheral on
separate Characteristics.
switch (central.state) {
case .poweredOn:
central.scanForPeripherals(withServices: [serviceUuid], options: nil) default:
}
}
func getNameFromAdvertisementData(
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
}
func centralManager(
_ central: CBCentralManager,
didDiscover peripheral: CBPeripheral,
advertisementData: [String : Any],
rssi RSSI: NSNumber)
{
// find the advertised name
if let advertisedName = getNameFromAdvertisementData(
advertisementData: advertisementData) {
if advertisedName == "EchoServer" {
centralManager.connect(peripheral, options: nil) }
}
}
Connecting
Discovering GATT
func peripheral(
_ peripheral: CBPeripheral, didDiscoverServices error: Error?)
{
// update the local copy of the Peripheral
connectedPeripheral = peripheral
if error != nil {
// error
} else {
for service in peripheral.services!{
if service.uuid == serviceUuid {
connectedPeripheral.discoverCharacteristics( [readCharacteristicUuid,
writeCharacteristicUuid],
for: service)
}
}
}
A list of Characteristics for each Service will come back in the peripheral
didDiscoverCharacteristicsFor callback in CBPeripheralManagerDelegate. Use
this to subscribe to the Read Characteristics and to save references to each
Characteristic:
func peripheral(
_ peripheral: CBPeripheral,
didDiscoverCharacteristicsFor service: CBService, error: Error?)
{
// update local copy of the peripheral
connectedPeripheral = peripheral
connectedPeripheral.delegate = self
// grab the service
let serviceIdentifier = service.uuid.uuidString
if let characteristics = service.characteristics {
} else if characteristic.uuid.uuidString == \
readCharacteristicUuid {
readCharacteristic = characteristic
connectedPeripheral.setNotifyValue(
true,
for: characteristic)
}
}
}
}
Writing Messages
To write a message to the Characteristic, convert the String message into a Data
object and write the value using the CBPeripheral.writeValue method:
// Flow control response
let flowControlMessage = "ready"
// outbound value to be sent to the Characteristic var outboundByteArray:
[UInt8]!
// packet offset in multi-packet value var packetOffset = 0
end = outboundByteArray.count
}
let transmissableValue = Data(Array(outboundByteArray[offset..<end]))
connectedPeripheral.writeValue(
transmissableValue,
for: writeCharacteristic)
Receiving Messages
{
if let value = characteristic.value {
let byteArray = [UInt8](value)
if let stringValue = String(data: value, encoding: .ascii) {
packetOffset += characteristicLength
if packetOffset < outboundByteArray.count { writePartialValue(
value: outboundByteArray,
offset: packetOffset)
}
}
}
}
The following code will create an App that connects to a Peripheral, allows a
user to type a message, send that message to the Peripheral, and then prints the
Peripheral’s response.
Figure 12-3.
CoreBluetooth Framework linked into project
Import the CoreBluetooth APIs in the code header:
import CoreBluetooth
Models
The BleCommManager turns the Bluetooth radio on and scans for nearby
Peripherals.
Example 12-1. Models/EchoServer.swift
import UIKit
import CoreBluetooth
class EchoServer:NSObject, CBPeripheralDelegate {
// MARK: Peripheral properties
// EchoServerDelegate
var delegate:EchoServerDelegate! // connected Peripheral
var connectedPeripheral:CBPeripheral! // connected Characteristic
var readCharacteristic:CBCharacteristic! var
writeCharacteristic:CBCharacteristic
/**
Initialize EchoServer with a corresponding Peripheral
- Parameters:
- delegate: The EchoServerDelegate
- peripheral: The discovered Peripheral
*/
init(delegate: EchoServerDelegate, peripheral: CBPeripheral) { super.init()
connectedPeripheral = peripheral
connectedPeripheral.delegate = self
self.delegate = delegate
/**
Notify the EchoServer that the peripheral has been connected */
}
/**
Get a advertised name from an advertisementData packet. This may be different
than the actual Peripheral name */
/**
Write a text value to the EchoServer
- Parameters:
- value: the value to write to the connected Characteristic
*/
func writeValue(value: String) {
// get the characteristic length
let writeableValue = value + "\n\0"
packetOffset = 0
// get the data for the current offset
outboundByteArray = Array(writeableValue.utf8) writePartialValue(value:
outboundByteArray, offset: packetOffset) }
/**
Write a partial value to the EchoServer
- Parameters:
- value: the full value to write to the connected Characteristic
- offset: the packet offset
*/
func writePartialValue(value: [UInt8], offset: Int) { // don't go past the total value
size
var end = offset + characteristicLength
if end > outboundByteArray.count {
end = outboundByteArray.count
}
let transmissableValue = \
isWriteableWithoutResponse: writeCharacteristic)
{
writeType = CBCharacteristicWriteType.withoutResponse
}
connectedPeripheral.writeValue(
transmissableValue,
for: writeCharacteristic,
type: writeType)
print("write request sent")
}
/**
Check if Characteristic is readable
- Parameters:
- characteristic: The Characteristic to test
- returns: True if characteristic is readable */
static func isCharacteristic(
isReadable characteristic: CBCharacteristic) -> Bool {
if (characteristic.properties.rawValue &
CBCharacteristicProperties.read.rawValue) != 0 {
print("readable")
return true
}
return false
}
/**
Check if Characteristic is writeable
- Parameters:
- characteristic: The Characteristic to test
- returns: True if characteristic is writeable */
static func isCharacteristic(
isWriteable characteristic: CBCharacteristic) -> Bool
{
print("testing if characteristic is writeable")
if (characteristic.properties.rawValue &
CBCharacteristicProperties.write.rawValue) != 0 ||
(characteristic.properties.rawValue &
CBCharacteristicProperties.writeWithoutResponse.rawValue) != 0
{
print("characteristic is writeable") return true
}
print("characteristic is not writeable") return false
}
/**
CBCharacteristicProperties.write.rawValue) != 0 {
return true }
return false }
/**
Check if Characteristic is writeable without response
- Parameters:
- characteristic: The Characteristic to test
- returns: True if characteristic is writeable without response
*/
static func isCharacteristic(
isWriteableWithoutResponse characteristic: CBCharacteristic) -> Bool {
if (characteristic.properties.rawValue &
CBCharacteristicProperties.writeWithoutResponse.rawValue) != 0 {
return true }
return false }
CBCharacteristicProperties.notify.rawValue) != 0 {
print("characteristic is notifiable")
return true
}
return false
}
// MARK: CBPeripheralDelegate
/**
Characteristic has been subscribed to or unsubscribed from */
func peripheral(
_ peripheral: CBPeripheral,
didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?)
{
connectedPeripheral = peripheral
connectedPeripheral.delegate = self
print("Notification state updated for: " + \
/**
Value was written to the Characteristic */
func peripheral(
_ peripheral: CBPeripheral,
didWriteValueFor descriptor: CBDescriptor, error: Error?)
{
print("data written")
}
/**
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)
let byteArray = [UInt8](value)
if let stringValue = String(data: value, encoding: .ascii) {
print(stringValue)
packetOffset += characteristicLength
print("new packet offset: \(packetOffset)")
print("new packet offset: \(packetOffset)")
if packetOffset < outboundByteArray.count {
"\(packetOffset)-\(byteArray.count)") writePartialValue(
value: outboundByteArray,
offset: packetOffset)
}
if delegate != nil {
delegate.echoServer(messageReceived: stringValue) }
}
}
}
/**
Servicess were discovered on the connected Peripheral */
func peripheral(
_ peripheral: CBPeripheral,
didDiscoverServices error: Error?)
{
print("services discovered")
connectedPeripheral = peripheral
connectedPeripheral.delegate = self
if error != nil {
print("Discover service Error: \(error)") } else {
print("Discovered Service")
for service in peripheral.services!{
if service.uuid == EchoServer.serviceUuid {
connectedPeripheral.discoverCharacteristics(
[EchoServer.readCharacteristicUuid, EchoServer.writeCharacteristicUuid], for:
service)
}
}
print(peripheral.services!)
print("DONE")
}
/**
Characteristics were discovered
for a Service on the connected Peripheral
*/
func peripheral(
_ peripheral: CBPeripheral,
didDiscoverCharacteristicsFor service: CBService,
error: Error?)
{
print("characteristics discovered")
connectedPeripheral = peripheral
connectedPeripheral.delegate = self
// grab the service
let serviceIdentifier = service.uuid.uuidString
print("service: \(serviceIdentifier)")
if let characteristics = service.characteristics { print("characteristics found: \
(characteristics.count)") for characteristic in characteristics {
print("-> \(characteristic.uuid.uuidString)") if characteristic.uuid.uuidString == \
EchoServer.writeCharacteristicUuid.uuidString {
print("matching uuid found for characteristic") writeCharacteristic =
characteristic
} else if characteristic.uuid.uuidString == \
EchoServer.readCharacteristicUuid.uuidString {
readCharacteristic = characteristic if EchoServer.isCharacteristic( isNotifiable:
characteristic) {
connectedPeripheral.setNotifyValue( true,
for: characteristic)
}
}
// notify the delegate
if readCharacteristic != nil && writeCharacteristic != nil {
if delegate != nil {
delegate.echoServer(connectedToCharacteristics: [readCharacteristic,
writeCharacteristic])
}
}
}
}
}
Delegates
Create an EchoServerDelegate that relays important events from the EchoServer.
Example 12-2. Delegates/EchoServerDelegate.swift
import UIKit
import CoreBluetooth
protocol EchoServerDelegate {
/**
Message received from Echo Server
- Parameters:
- stringValue: the value read from the Charactersitic */
- Parameters:
- characteristic: the Characteristic that was subscribed/unsubscribed */
func echoServer(
connectedToCharacteristics characteristics: [CBCharacteristic]) }
Storyboard
Create and link the UIButtons, UILabels, UITextViews, and UITextFields in the
UIView in the Main.storyboard to create the App's user interface (Figure 12-4).
Figure 12-4. Project Storyboard
Controllers
The App will indicate a connection status prior to connecting to the Peripheral.
After connection, UIButtons and UITextFields are displayed that enable
interaction with the Peripheral.
// MARK: UI Elements
@IBOutlet weak var characteristicValueText: UITextView! @IBOutlet weak var
writeCharacteristicButton: UIButton! @IBOutlet weak var
writeCharacteristicText: UITextField!
/**
View loaded
*/
}
/**
Write button pressed
*/
@IBAction func onWriteCharacteristicButtonTouchUp(_ sender: UIButton) {
print("write button pressed")
if let string = writeCharacteristicText.text {
print(string)
echoServer.writeValue(value: string)
writeCharacteristicText.text = ""
}
}
// MARK: EchoServerDelegate
/**
Message received from EchoServer. Update UI
*/
NSMakeRange(stringLength-1, 0))
}
/**
Characteristic was connected on the EchoServer. Update UI */
func echoServer(
connectedToCharacteristics characteristics: [CBCharacteristic])
{
for characteristic in characteristics {
print(" characteristic: -> " + \
"\(characteristic.uuid.uuidString): " +
"\(characteristic.properties.rawValue)")
if EchoServer.isCharacteristic(isWriteable: characteristic) {
writeCharacteristicText.isEnabled = true
writeCharacteristicButton.isEnabled = true
}
}
}
// MARK: CBCentralManagerDelegate
/**
centralManager is called each time a new Peripheral is discovered
- parameters
- central: the CentralManager for this UIView
- peripheral: A discovered Peripheral
- advertisementData: Bluetooth advertisement discovered with Peripheral
- 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))")
echoServer = EchoServer(delegate: self, peripheral: peripheral) // find the
advertised name
if let advertisedName = EchoServer.getNameFromAdvertisementData(
advertisementData: advertisementData)
{
if advertisedName == EchoServer.advertisedName { print("connecting to
peripheral...") centralManager.connect(peripheral, options: nil)
}
}
}
/**
Peripheral connected.
- Parameters:
- central: the reference to the central
- peripheral: the connected Peripheral */
func centralManager(
_ central: CBCentralManager, didConnect peripheral: CBPeripheral)
{
print("Connected Peripheral: \(peripheral.name)") // Do any additional setup
after loading the view. echoServer.connected(peripheral: peripheral)
}
/**
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")
writeCharacteristicButton.isEnabled = false
}
/**
Bluetooth radio state changed
- Parameters:
- central: the reference to the central */
print("bluetooth on")
central.scanForPeripherals(
withServices: [EchoServer.serviceUuid], options: nil)
default:
print("bluetooth unavailable")
}
}
}
The resulting Central App can scan for and connect to a Bluetooth Low Energy
Peripheral. Once connected, the Central can send and receive messages to the
Peripheral.
If a user types “hello” into the bottom TextView and hit the Subscribe button, the
word “hello” will appear above in the upper Text View (
Figure 12-5).
Figure 12-5. App screen showing interface to read from and write to the
Echo Server
Programming the Peripheral
The Echo Server will handle incoming writes on one Characteristic (0x2a57) and
echo back messages on another Characteristic (0x2a56).
It will also host a minimal GATT Profile that includes a battery percentage,
device name, model number, and serial number.
The Peripheral must advertise and host a GATT Profile, which will include a
minimal GATT profile.
The Peripheral will host a read-only, notifiable Characteristic on UUID 0x2a56
and a write-only Characteristic on UUID 0x2a57:
// service UUIDs
let serviceUuid = CBUUID(string: "0000180c-0000-1000-8000-00805f9b34fb")
// Characteristic UUIDs
let readCharacteristicUuid = CBUUID(
string: "00002a56-0000-1000-8000-00805f9b34fb")
type: writeCharacteristicUuid,
properties: wProperties,
value: nil,
permissions: wPermissions)
CBAdvertisementDataLocalNameKey: advertisingName,
CBAdvertisementDataServiceUUIDsKey: serviceUuids ]
// Start advertising
peripheralManager.startAdvertising(advertisementData)
/**
Connected Central subscribed to a Characteristic */
func peripheralManager(
_ peripheral: CBPeripheralManager,
central: CBCentral,
didSubscribeTo characteristic: CBCharacteristic)
{
self.central = central
}
/**
Connected Central unsubscribed from a Characteristic */
func peripheralManager(
_ peripheral: CBPeripheralManager,
central: CBCentral,
didUnsubscribeFrom characteristic: CBCharacteristic)
{
self.central = central
}
/**
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) print("new
request")
if let value = request.value {
// update readCharacteristic
print("updating characteristic")
readCharacteristic.value = value
// notify subscribers
print("notifying characteristic")
peripheralManager.updateValue(
value,
for: readCharacteristic,
onSubscribedCentrals: [central])
}
}
}
Read requests are handled by responding with a status message regarding the
outcome of the read request and the value of the Characteristic if the read request
was successful:
/**
Connected Central requested to read from a Characteristic */
func peripheralManager(
_ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest)
{
let characteristic = request.characteristic
if (characteristic.uuid == readCharacteristic.uuid) {
}
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) }
}
}
The following code will create an App that advertises a Peripheral, allows a
Central to connect and write values to a writeable Characteristic, and will
respond by copying that value to a readable, notifiable Characteristic.
Create a new project called EchoServer and create the following file structure (
Figure 12-6):
Figure
12-6. Project Structure
Frameworks
Import the CoreBluetooth Framework (Figure 12-7).
Figure 3-7.
CoreBluetooth Framework linked into project
Import the CoreBluetooth APIs in the code header:
import CoreBluetooth
Models
The EchoServer class will define the GATT Profile of the Echo Server, and will
react to subscriptions, reads, and write events. Write events will set read
Characteristic to the newly written Characteristic value and notify the connected
Central of the change.
- Parameters:
- delegate: The BlePeripheralDelegate
- peripheral: The discovered Peripheral
*/
init(delegate: EchoServerPeripheralDelegate?) { super.init()
// empty dispatch queue let dispatchQueue:DispatchQueue! = nil
]
peripheralManager = CBPeripheralManager(
delegate: self,
queue: dispatchQueue,
options: options)
self.delegate = delegate
}
/**
Stop advertising, shut down the Peripheral */
func stop() {
peripheralManager.stopAdvertising()
}
/**
Start Bluetooth Advertising.
This must be after building the GATT profile
*/
func startAdvertising() {
let serviceUuids = [serviceUuid]
let advertisementData:[String: Any] = [
CBAdvertisementDataLocalNameKey: advertisingName,
CBAdvertisementDataServiceUUIDsKey: serviceUuids ]
peripheralManager.startAdvertising(advertisementData) }
/**
Build Gatt Profile.
This must be done after Bluetooth Radio has turned on
*/
func buildGattProfile() {
let service = CBMutableService(type: serviceUuid, primary: true)
type: readCharacteristicUuid,
properties: rProperties,
value: nil,
permissions: rPermissions)
type: writeCharacteristicUuid,
properties: wProperties,
value: nil,
permissions: wPermissions)
]
peripheralManager.add(service) }
// MARK: CBPeripheralManagerDelegate
func peripheralManager(
_ peripheral: CBPeripheralManager, willRestoreState dict: [String : Any])
{
print("restoring peripheral state")
}
/**
Peripheral added a new Service
*/
func peripheralManager(
_ peripheral: CBPeripheralManager, didAdd service: CBService,
error: Error?)
{
print("added service to peripheral") if error != nil {
print(error.debugDescription) }
}
/**
Peripheral started advertising
*/
}
self.peripheralManager = peripheral delegate?.echoServerPeripheral?
(startedAdvertising: error) }
/**
Connected Central requested to read from a Characteristic */
func peripheralManager(
_ peripheral: CBPeripheralManager,
didReceiveRead request: CBATTRequest)
{
let characteristic = request.characteristic if (characteristic.uuid ==
readCharacteristic.uuid) {
}
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)
}
}
}
/**
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) print("new
request")
if let value = request.value {
print("notifying delegate")
delegate?.echoServerPeripheral?(
valueWritten: value,
toCharacteristic: request.characteristic)
value,
for: readCharacteristic, onSubscribedCentrals: nil)
}
}
}
/**
Connected Central subscribed to a Characteristic */
func peripheralManager(
_ peripheral: CBPeripheralManager,
central: CBCentral,
didSubscribeTo characteristic: CBCharacteristic)
{
self.central = central
}
{
self.central = central
}
/**
Peripheral is about to notify subscribers
of changes to a Characteristic
*/
func peripheralManagerIsReady(
toUpdateSubscribers peripheral: CBPeripheralManager)
{
print("Peripheral about to update subscribers")
}
/**
Bluetooth Radio state changed
*/
} }
Delegates
The BlePeripheralDelegate relays state changes and events from the Echo
Server, such as when the Echo Server begins Advertising and when data has
been written:
Example 12-5. Delegates/EchoServerPeripheralDelegate.swift
import UIKit
import CoreBluetooth
@objc protocol EchoServerPeripheralDelegate : class {
/**
Echo Server State Changed
- Parameters:
- rssi: the RSSI
- blePeripheral: the BlePeripheral
*/
@objc optional func echoServerPeripheral( stateChanged state:
CBManagerState)
/**
Echo Server statrted advertising
- Parameters:
- error: the error message, if any */
Storyboard
Create the UISwitches, UITextViews, and UILabels in the UIView in the
Main.storyboard to create the App's user interface (Figure 12-8).
Figure 12-8. Project Storyboard
Controllers
The View will display a log of the incoming text written to the EchoServer's
write Characteristic (0x2a57) as well as a switch indicating the Advertising state
of the EchoServer (Figure 12-9).
// MARK: UI Elements
@IBOutlet weak var advertisingLabel: UILabel! @IBOutlet weak var
advertisingSwitch: UISwitch! @IBOutlet weak var characteristicLogText:
UITextView!
// MARK: BlePeripheral
// BlePeripheral
var echoServer:EchoServerPeripheral!
/**
UIView loaded
*/
/**
View appeared. Start the Peripheral
*/
/**
View will appear. Stop transmitting random data */
/**
View disappeared. Stop advertising
*/
// MARK: BlePeripheralDelegate
/**
Echo Server state changed
- Parameters:
- state: the CBManagerState representing the new state */
- Parameters:
- error: the error message, if any
*/
} else {
print("adertising started")
advertisingSwitch.setOn(true, animated: true)
}
}
/**
Value written to Characteristic
- Parameters:
- stringValue: the value read from the Charactersitic
- characteristic: the Characteristic that was written to
*/
func echoServerPeripheral(
valueWritten value: Data,
toCharacteristic: CBCharacteristic)
{
print("converting data to String")
let stringValue = String(data: value, encoding: .utf8) if let stringValue =
stringValue {
print("writing to textview")
characteristicLogText.text = characteristicLogText.text + \ "\n" + stringValue
if !characteristicLogText.text.isEmpty {
characteristicLogText.scrollRangeToVisible( NSMakeRange(0, 1))
} }
}
}
Because binary data it is the language of computers, it is easier to work with than
text. There is no need to worry about character sets, null characters, or cut-off
words.
This project will show how to remotely control an LED on a Peripheral using
software on a Central.
The LED Remote works like this (
Figure 13-1).
Figure 13-1. How a Remote Control LED works
In all the other examples, text was being sent between Central and Peripheral.
In order for the Central and Peripheral to understand each other, they need
shared language between them. In this case, a data packet format.
The Peripheral then responds to the Central with a status message regarding the
success or failure to execute the command. This can also be expressed as two
bytes (Figure 13-3).
Gatt Profile
UUID Use
0x2a56 Send commands from Central to Peripheral 0x2a57 Send responses from
Peripheral to Central
Programming the Central
This project shows how to send commands to a Peripheral from a Central.
GATT Profile
The GATT Profile will be set up as a digital input/output under the Automation
IO (0x1815) Service, witch commands to and responses from the Peripheral on
separate Characteristics.
Data Formatting
In order to read and write binary commands to the Peripheral, it and the Central
must understand the same messages and formatting.
// the size of the characteristic let characteristicLength = 2
Enable Bluetooth
To turn on the Bluetooth radio programmatically, also enable Bluetooth admin
permissions:
switch (central.state) {
case .poweredOn:
central.scanForPeripherals(withServices: [serviceUuid], options: nil) default:
}
}
func getNameFromAdvertisementData(
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
}
func centralManager(
_ central: CBCentralManager,
didDiscover peripheral: CBPeripheral,
advertisementData: [String : Any],
rssi RSSI: NSNumber)
{
// find the advertised name
if let advertisedName = getNameFromAdvertisementData(
advertisementData: advertisementData) {
if advertisedName == "RemoteLed" {
centralManager.connect(peripheral, options: nil)
}
}
}
Connecting
Discovering GATT
func peripheral(
_ peripheral: CBPeripheral, didDiscoverServices error: Error?)
{
// update the local copy of the Peripheral connectedPeripheral = peripheral
if error != nil {
// error
} else {
for service in peripheral.services!{
if service.uuid == serviceUuid {
connectedPeripheral.discoverCharacteristics( [readCharacteristicUuid,
writeCharacteristicUuid],
for: service)
}
}
}
}
A list of Characteristics for each Service will come back in the peripheral
didDiscoverCharacteristicsFor callback in CBPeripheralManagerDelegate. Use
this to subscribe to the Read Characteristics and to save references to each
Characteristic:
func peripheral(
_ peripheral: CBPeripheral,
didDiscoverCharacteristicsFor service: CBService, error: Error?)
{
// update local copy of the peripheral connectedPeripheral = peripheral
connectedPeripheral.delegate = self
// grab the service
let serviceIdentifier = service.uuid.uuidString
{ commandCharacteristic = characteristic
} else if characteristic.uuid.uuidString == \ responseCharacteristicUuid
{
responseharacteristic = characteristic
connectedPeripheral.setNotifyValue( true,
for: characteristic)
}
}
}
}
Sending Commands
To write the command, build a byte array based on the defined command data
structure, and convert to a Data object. Then write the value using the
CBPeripheral.writeValue method:
}
}
Receiving Responses
func peripheral(
_ peripheral: CBPeripheral, d
idUpdateValueFor characteristic: CBCharacteristic, error: Error?)
{
if let value = characteristic.value { let responseValue = [UInt8](value)
// decode message
let responseType = responseValue[bleResponseFooterPosition] switch
responseType {
case bleResponseConfirmationFooter:
let response = responseValue[bleResponseDataPosition]
The following code will create a single Activity App with a toggle switch that
connects to a Peripheral. Once connected the user can flip the toggle back and
fourth, which issues a command to the Peripheral to turn an LED on or off. The
switch changes state when the App receives confirmation that the
Figure 13-5.
CoreBluetooth Framework linked into project
Import the CoreBluetooth APIs in the code header:
import CoreBluetooth
Objects
The BleCommManager turns the Bluetooth radio on and scans for nearby
Peripherals.
Example 13-1. Models/RemoteLed.swift
import UIKit
import CoreBluetooth
class RemoteLed:NSObject, CBPeripheralDelegate {
// MARK: Peripheral properties
// RemateLedDelegate
var delegate:RemoteLedDelegate!
// connected Peripheral
var peripheral:CBPeripheral!
// connected Characteristic
var commandCharacteristic:CBCharacteristic! var
responseCharacteristic:CBCharacteristic!
/**
Initialize EchoServer with a corresponding Peripheral
- Parameters:
- delegate: The RemoteLEDPeripheral
- peripheral: The discovered Peripheral
*/
init(delegate: RemoteLedDelegate, peripheral: CBPeripheral) { super.init()
self.delegate = delegate
self.peripheral = peripheral
self.peripheral.delegate = self
/**
Notify the RemoteLed that the peripheral has been connected */
/**
Get a advertised name from an advertisementData packet. This may be different
than the actual Peripheral name */
/**
writeCommand(ledCommandState: bleCommandLedOn) }
/**
Turn the remote LED off
*/
func turnLedOff() {
writeCommand(ledCommandState: bleCommandLedOff) }
/**
Write a command to the remote
*/
isWriteableWithoutResponse: commandCharacteristic) {
writeType = CBCharacteristicWriteType.withoutResponse }
peripheral.writeValue(
value,
for: commandCharacteristic,
type: writeType)
}
}
CBCharacteristicProperties.read.rawValue) != 0 {
print("readable")
return true
}
return false
}
/**
Check if Characteristic is writeable
- Parameters:
- characteristic: The Characteristic to test
- returns: True if characteristic is writeable */
static func isCharacteristic(
isWriteable characteristic: CBCharacteristic) -> Bool
{
print("testing if characteristic is writeable")
if (characteristic.properties.rawValue & \
CBCharacteristicProperties.write.rawValue) != 0 ||
(characteristic.properties.rawValue & \
CBCharacteristicProperties.writeWithoutResponse.rawValue) != 0
{
print("characteristic is writeable") return true
}
print("characetiristic is not writeable") return false
}
/**
Check if Characteristic is writeable with response
- Parameters:
- characteristic: The Characteristic to test
- returns: True if characteristic is writeable with response */
static func isCharacteristic(
isWriteableWithResponse characteristic: CBCharacteristic) -> Bool {
if (characteristic.properties.rawValue & \
CBCharacteristicProperties.write.rawValue) != 0 {
return true }
return false }
/**
Check if Characteristic is writeable without response
- Parameters:
- characteristic: The Characteristic to test
- returns: True if characteristic is writeable without response */
static func isCharacteristic(
isWriteableWithoutResponse characteristic: CBCharacteristic) -> Bool {
if (characteristic.properties.rawValue & \
CBCharacteristicProperties.writeWithoutResponse.rawValue) != 0 {
return true }
return false }
/**
Check if Characteristic is notifiable
- Parameters:
- characteristic: The Characteristic to test
- returns: True if characteristic is notifiable */
static func isCharacteristic(
isNotifiable characteristic: CBCharacteristic) -> Bool {
if (characteristic.properties.rawValue & \
CBCharacteristicProperties.notify.rawValue) != 0 {
return true }
return false }
// 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)")
}
}
/**
Value downloaded from Characteristic on connected Peripheral */
func peripheral(
_ peripheral: CBPeripheral,
didUpdateValueFor characteristic: CBCharacteristic, error: Error?)
{
if let value = characteristic.value { print(value.debugDescription)
print(value.description as String)
let responseValue = [UInt8](value)
// decode message
let responseType = responseValue[bleResponseFooterPosition] switch
responseType {
case bleResponseConfirmationFooter:
/**
Servicess were discovered on the connected Peripheral */
func peripheral(
_ peripheral: CBPeripheral,
didDiscoverServices error: Error?)
{
print("services discovered")
if error != nil {
print("Discover service Error: \(error)")
} else {
print("Discovered Service")
for service in peripheral.services!{
if service.uuid == RemoteLed.serviceUuid {
self.peripheral.discoverCharacteristics( [
RemoteLed.commandCharacteristicUuid,
RemoteLed.responseCharacteristicUuid ],
for: service)
}
}
print(peripheral.services!)
print("DONE")
}
}
/**
Characteristics were discovered
for a Service on the connected Peripheral
*/
func peripheral(
_ peripheral: CBPeripheral,
didDiscoverCharacteristicsFor service: CBService, error: Error?)
{
print("characteristics discovered")
if characteristic.uuid == \
RemoteLed.commandCharacteristicUuid {
commandCharacteristic = characteristic } else if characteristic.uuid == \
RemoteLed.responseCharacteristicUuid {
responseCharacteristic = characteristic
print(" -> \(characteristic.uuid.uuidString): " + "\
(characteristic.properties.rawValue)")
if RemoteLed.isCharacteristic(
isNotifiable: responseCharacteristic)
{
self.peripheral.setNotifyValue( true,
for: responseCharacteristic)
}
}
}
delegate.remoteLed(connectedToCharacteristics: [ responseCharacteristic,
commandCharacteristic
])
}
}
}
Delegates
Create an RemoteLedDelegate that relays important events from the RemoteLed.
Example 13-2. Delegates/RemoteLedDelegate.swift
import UIKit
import CoreBluetooth
protocol RemoteLedDelegate {
/**
Characteristic was connected on the Remote LED
- Parameters:
- characteristic: the connected Characteristic
*/
func remoteLed(
connectedToCharacteristics characteristics: [CBCharacteristic])
/**
Error received from Remote
- Parameters
- messageValue: an error response */
func remoteLed(errorReceived messageValue: String)
/**
Remote command was successful and a response was issued
- Parameters:
- ledState: one of RemoteLed.ledOn or RemoteLed.ledOff
*/
func remoteLed(confirmationReceived ledState: UInt8) }
Storyboard
Create the UISwitches, UITextViews, and UILabels in the UIView in the
Main.storyboard to create the App's user interface (Figure 13-6):
Figure 13-6. Project Storyboard
Controllers
The Main activity will have a switch and a label describing what the switch
does. When the user toggles the switch, the UIViewController will issue a
command to the Peripheral to turn its LED on or off.
CBCentralManagerDelegate, RemoteLedDelegate {
// MARK: UI Components
@IBOutlet weak var identifierLabel: UILabel! @IBOutlet weak var
ledStateSwitch: UISwitch!
/**
View loaded
*/
/**
LED switch toggled
*/
} else {
print("led switched off")
remoteLed.turnLedOff()
}
}
// MARK: RemoteLedDelegate
/**
Characteristic was connected on the Remote LED. Update UI */
func remoteLed(
connectedToCharacteristics characteristics: [CBCharacteristic])
{
ledStateSwitch.isEnabled = true remoteLed.turnLedOn()
}
/**
Error received from Remote. Update UI
*/
/**
Remote command was successful and a response was issued. Update UI */
} else {
print("led turned off")
ledStateSwitch.isOn = false
}
ledStateSwitch.isEnabled = true
}
- parameters
- central: the CentralManager for this UIView
- peripheral: A discovered Peripheral
- advertisementData: Bluetooth advertisement found with Peripheral
- 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.name)")
print("Discovered \(peripheral.identifier.uuidString) " + \ "(\(peripheral.name))")
remoteLed = RemoteLed(delegate: self, peripheral: peripheral)
// find the advertised name
if let advertisedName = RemoteLed.getNameFromAdvertisementData(
advertisementData: advertisementData)
{
if advertisedName == RemoteLed.advertisedName { print("connecting to
peripheral...") centralManager.connect(peripheral, options: nil) }
}
}
/**
Peripheral connected.
- Parameters:
- central: the reference to the central
- peripheral: the connected Peripheral */
func centralManager(
_ central: CBCentralManager, didConnect peripheral: CBPeripheral)
{
print("Connected Peripheral: \(peripheral.name)")
remoteLed.connected(peripheral: peripheral)
// Do any additional setup after loading the view.
identifierLabel.text = remoteLed.peripheral.identifier.uuidString
}
/**
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")
}
/**
Bluetooth radio state changed
- Parameters:
- central: the reference to the central
*/
case .poweredOn:
print("bluetooth on")
centralManager.scanForPeripherals(
The resulting App scan for and connect to a Bluetooth Low Energy Peripheral.
Once connected, the user can turn the Peripheral’s LED on or off using the
switch. When the LED turns on. The switch fully moves when the App receives
confirmation from the Peripheral that the LED state has changed (Figure 13-7).
Figure 13-7. App screen showing LED switch in on and off states
Programming the Peripheral
This project shows how to process commands sent from a Central, and respond
with status confirmations.
Data Formatting
In order to read and write binary commands to the Peripheral, it and the Central
must understand the same messages and formatting. To best emulate a typical
Bluetooth device, UInt8 data types are used to represent the binary commands.
Regular Integers are used for positions:
// MARK: Commands
// Data Positions
let bleCommandFooterPosition = 1; let bleCommandDataPosition = 0;
// Command flag
let bleCommandFooter:UInt8 = 1;
// LED State
let bleCommandLedOn:UInt8 = 1; let bleCommandLedOff:UInt8 = 2;
// MARK: Response
// Data Positions
let bleResponseFooterPosition = 1; let bleResponseDataPosition = 0;
// Response Types
let bleResponseErrorFooter:UInt8 = 0; let
bleResponseConfirmationFooter:UInt8 = 1;
// LED States
let bleResponseLedError:UInt8 = 0; let bleResponseLedOn:UInt8 = 1; let
bleResponseLedOff:UInt8 = 2;
// Command Characteristic
var commandCharacteristic:CBMutableCharacteristic!
// Response Characteristic
var responseCharacteristic:CBMutableCharacteristic!
// create Service
let service = CBMutableService(type: serviceUuid, primary: true)
type: responseCharacteristicUuid,
properties: rProperties,
value: nil,
permissions: rPermissions)
type: commandCharacteristicUuid,
properties: cProperties,
value: nil,
permissions: cPermissions)
CBAdvertisementDataLocalNameKey: advertisingName,
CBAdvertisementDataServiceUUIDsKey: serviceUuids ]
// start Advertising
peripheralManager.startAdvertising(advertisementData)
func peripheralManager(
_ peripheral: CBPeripheralManager,
central: CBCentral,
didSubscribeTo characteristic: CBCharacteristic)
{
self.central = central
}
func peripheralManager(
_ peripheral: CBPeripheralManager, didReceiveWrite requests:
[CBATTRequest])
{
for request in requests {
peripheral.respond(to: request, withResult: CBATTError.success) if let value =
request.value {
case bleCommandLedOn:
print("Turning LED on") setLedState(ledState: true)
case bleCommandLedOff:
print("Turning LED off") setLedState(ledState: false)
default:
print("Unknown command value")
}
}
}
try device.lockForConfiguration()
try device.setTorchModeOnWithLevel(1.0)
if ledState {
} else {
print("Led turned off")
device.torchMode = AVCaptureTorchMode.off ledStateSwitch.setOn(false,
animated: true)
}
device.unlockForConfiguration()
The following code will create an App that turns the camera flash on and o ff
when a command is received over a writeable Characteristic. A readable
Characteristic is used to respond and notify the Connected central of the updated
LED state.
Create a new project called RemoteLed with the following project structure (
Figure 13-8).
Figure 13-
8. Project structure
Frameworks
Import the CoreBluetooth Framework (Figure 13-9):
Figure 13-9.
CoreBluetooth Framework linked into project
Import the CoreBluetooth APIs in the code header:
import CoreBluetooth
Models
The RemoteLedPeripheral class will define the GATT Profile for the Peripheral,
and will react to read, write, and subscription events:
Example 13-4. Models/RemoteLedPeripheral.swift
import UIKit
import CoreBluetooth
class RemoteLedPeripheral : NSObject, CBPeripheralManagerDelegate {
// MARK: Peripheral properties
// Advertized name
let advertisingName = "LedRemote"
// Device identifier
let peripheralIdentifier = "8f68d89b-448c-4b14-aa9a-f8de6d8a4753"
// Service UUID
let serviceUuid = CBUUID(string: "00001815-0000-1000-8000-00805f9b34fb")
// Characteristic UUIDs
let commandCharacteristicUuid =
// MARK: Commands
// Data Positions
let bleCommandFooterPosition = 1; let bleCommandDataPosition = 0; //
Command flag
let bleCommandFooter:UInt8 = 1; // LED State
let bleCommandLedOn:UInt8 = 1; let bleCommandLedOff:UInt8 = 2;
// MARK: Response
// Data Positions
let bleResponseFooterPosition = 1; let bleResponseDataPosition = 0; // Response
Types
let bleResponseErrorFooter:UInt8 = 0; let
bleResponseConfirmationFooter:UInt8 = 1; // LED States
let bleResponseLedError:UInt8 = 0;
let bleResponseLedOn:UInt8 = 1;
let bleResponseLedOff:UInt8 = 2
// Peripheral Manager
var peripheralManager:CBPeripheralManager! // Connected Central
var central:CBCentral!
// delegate
var delegate:RemoteLedPeripheralDelegate!
/**
Initialize BlePeripheral with a corresponding Peripheral
- Parameters:
- delegate: The BlePeripheralDelegate
- peripheral: The discovered Peripheral
*/
init(delegate: RemoteLedPeripheralDelegate?) { super.init()
// empty dispatch queue
let dispatchQueue:DispatchQueue! = nil
// Build Advertising options
let options:[String : Any] = [
peripheralIdentifier
]
peripheralManager = CBPeripheralManager(
delegate: self,
queue: dispatchQueue, options: options)
self.delegate = delegate }
/**
Stop advertising, shut down the Peripheral */
func stop() {
peripheralManager.stopAdvertising()
}
/**
Start Bluetooth Advertising.
This must be after building the GATT profile
*/
func startAdvertising() {
let serviceUuids = [serviceUuid]
let advertisementData:[String: Any] = [
CBAdvertisementDataLocalNameKey: advertisingName,
CBAdvertisementDataServiceUUIDsKey: serviceUuids ]
peripheralManager.startAdvertising(advertisementData) }
/**
Build Gatt Profile.
This must be done after Bluetooth Radio has turned on
*/
func buildGattProfile() {
let service = CBMutableService(type: serviceUuid, primary: true) var rProperties
= CBCharacteristicProperties.read
rProperties.formUnion(CBCharacteristicProperties.notify) var rPermissions =
CBAttributePermissions.writeable
rPermissions.formUnion(CBAttributePermissions.readable)
responseCharacteristic = CBMutableCharacteristic(
type: responseCharacteristicUuid,
properties: rProperties,
value: nil,
permissions: rPermissions)
let cProperties = CBCharacteristicProperties.write let cPermissions =
CBAttributePermissions.writeable commandCharacteristic =
CBMutableCharacteristic(
type: commandCharacteristicUuid,
properties: cProperties,
value: nil,
permissions: cPermissions)
service.characteristics = [
responseCharacteristic,
commandCharacteristic
]
peripheralManager.add(service)
}
/**
Make sense of the incoming byte array as a command
*/
- Parameters:
- ledState: *true* for on, *false* for off
*/
} else {
sendBleResponse(ledState: bleResponseLedOff)
}
delegate?.remoteLedPeripheral?(ledStateChangedTo: ledState) }
/**
Send a formatted response out via a Bluetooth Characteristic
- Parameters
- ledState: one of bleResponseLedOn or bleResponseLedOff */
responseArray[bleResponseFooterPosition] = \ bleResponseConfirmationFooter
responseArray[bleResponseDataPosition] = ledState
let value = Data(bytes: responseArray)
responseCharacteristic.value = value
peripheralManager.updateValue(
value,
for: responseCharacteristic,
onSubscribedCentrals: nil)
// MARK: CBPeripheralManagerDelegate
/**
Peripheral will become active
*/
func peripheralManager(
_ peripheral: CBPeripheralManager, willRestoreState dict: [String : Any])
{
print("restoring peripheral state")
}
/**
Peripheral added a new Service
*/
func peripheralManager(
_ peripheral: CBPeripheralManager, didAdd service: CBService,
error: Error?)
{
print("added service to peripheral") if error != nil {
print(error.debugDescription) }
}
/**
Peripheral started advertising
*/
{
if error != nil {
print ("Error advertising peripheral")
print(error.debugDescription) }
self.peripheralManager = peripheral
delegate?.remoteLedPeripheral?(startedAdvertising: error) }
/**
Connected Central requested to read from a Characteristic */
func peripheralManager(
_ peripheral: CBPeripheralManager,
didReceiveRead request: CBATTRequest)
{
let characteristic = request.characteristic
if (characteristic.uuid == responseCharacteristic.uuid) {
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)
}
}
}
func peripheralManager(
_ peripheral: CBPeripheralManager,
didReceiveWrite requests: [CBATTRequest])
{
for request in requests {
peripheral.respond(to: request, withResult: CBATTError.success) print("new
request")
if let value = request.value {
/**
Connected Central subscribed to a Characteristic */
func peripheralManager(
_ peripheral: CBPeripheralManager,
central: CBCentral,
didSubscribeTo characteristic: CBCharacteristic)
{
self.central = central
}
/**
Connected Central unsubscribed from a Characteristic */
func peripheralManager(
_ peripheral: CBPeripheralManager,
central: CBCentral,
didUnsubscribeFrom characteristic: CBCharacteristic)
{
self.central = central }
/**
Peripheral is about to notify subscribers of changes to a Characteristic
*/
func peripheralManagerIsReady(
toUpdateSubscribers peripheral: CBPeripheralManager)
{
print("Peripheral about to update subscribers")
}
/**
Bluetooth Radio state changed
*/
} }
Delegates
- Parameters:
- rssi: the RSSI
- blePeripheral: the BlePeripheral
*/
@objc optional func remoteLedPeripheral( stateChanged state:
CBManagerState)
/**
RemoteLed statrted advertising
- Parameters:
- error: the error message, if any */
- Parameters:
- ledState: *true* for on, *false* for off */
Storyboard
Create the UISwitches, UITextViews, and UILabels in the UIView in the
Main.storyboard to create the App's user interface (Figure 13-10).
Figure 13-10. Project Storyboard
Controllers
The View will display the advertising state and LED state with UISwitches, and
will turn the camera flash on and off in response to the incoming LED state:
Example 13-6. UI/Controllers/ViewController.swift
import UIKit
import CoreBluetooth import AVFoundation
// MARK: UI Elements
@IBOutlet weak var advertisingLabel: UILabel! @IBOutlet weak var
advertisingSwitch: UISwitch! @IBOutlet weak var ledStateSwitch: UISwitch!
// MARK: BlePeripheral
// BlePeripheral
var remoteLed:RemoteLedPeripheral!
/**
UIView loaded
*/
/**
View appeared. Start the Peripheral
*/
/**
View will appear. Stop transmitting random data */
/**
View disappeared. Stop advertising
*/
// MARK: BlePeripheralDelegate
/**
RemoteLed state changed
- Parameters:
- state: the CBManagerState representing the new state */
/**
RemoteLed statrted adertising
- Parameters:
- error: the error message, if any
*/
} else {
print("adertising started")
advertisingSwitch.setOn(true, animated: true)
}
}
/**
Led State Changed
- Parameters:
- stringValue: the value read from the Charactersitic
- characteristic: the Characteristic that was written to
*/
func remoteLedPeripheral(ledStateChangedTo ledState: Bool) { if let device =
AVCaptureDevice.defaultDevice(
withMediaType: AVMediaTypeVideo)
{
if (device.hasTorch) { do {
try device.lockForConfiguration()
try device.setTorchModeOnWithLevel(1.0)
if ledState {
print("Led turned on")
device.torchMode = AVCaptureTorchMode.on ledStateSwitch.setOn(true,
animated: true) } else {
print("Led turned off")
device.torchMode = AVCaptureTorchMode.off ledStateSwitch.setOn(false,
animated: true) }
device.unlockForConfiguration()
} catch let error as NSError {
print("problem locking camera: "+error.debugDescription) }
}
} }
The resulting App can turn the camera flash on and o ff in response to
commands from a connected Central. When the command is processed, a
response is sent through the response Characteristic (Figure 13-11).
Figure 13-11. App screen showing LED in on and off states
Example code
The code for this chapter is available online
at: https://github.com/BluetoothLowEnergyIniOSSwift/Chapter13
Appendix
For reference, the following are properties of the Bluetooth Low Energy network
and hardware.
Range 100 m (330 ft)
Data Rate 1M bit/s
Application Throughput 0.27 Mbit/s
Security
128-bit AES with Counter Mode CBC-MAC and application layer user defined
(BEWARE: this encryption has vulnerabilities)
Robustness
Adaptive Frequency Hopping, Lazy Acknowledgement, 24-bit CRC, 32-bit
Message Integrity Check
Range 100 m (330 ft)
Data Rate 1M bit/s
Application Throughput 0.27 Mbit/s
Security
Peak Current Consumption
Byte-Order in Broadcast
Range
Data Rate
Application Throughput
Security
128-bit AES with Counter Mode CBC-MAC and application layer user defined
(BEWARE: this encryption has vulnerabilities)
< 15 mA
Big Endian (most significant bit at end)
100 m (330 ft)
1M bit/s
0.27 Mbit/s
128-bit AES with Counter Mode CBC-MAC and application layer user defined
(BEWARE: this encryption has vulnerabilities)
Source: Wikipedia: Bluetooth_Low_Energy Retrieved from
https://en.wikipedia.org/wiki/Bluetooth_low_energy
Appendix II: UUID Format
Bluetooth Low Energy has tight space requirements. Therefore it is preferred to
transmit 16-bit UUIDs instead of 32-bit UUIDs. UUIDs can be converted
between 16-bit and 32-bit with the standard Bluetooth Low Energy UUID
format:
These Services UUIDs have been reserved for special contexts, such as Device
Information (0x180A) Which may contain Characteristics that communicate
information about the Peripheral's name, version number, or settings.
Note: All Bluetooth Peripherals should have a Battery Service (0x180F) Service
containing a Battery Level (0x2A19) Characteristic.
Table IV-1. Reserved GATT Services
0x2AA6
org.bluetooth.characteristic.gap.central_address_resolution_su pport
0x2A2A
org.bluetooth.characteristic.ieee_11073-20601_regulatory_cert
ification_data_list
0x2AAD org.bluetooth.characteristic.indoor_positioning_configuration
0x2A36 org.bluetooth.characteristic.intermediate_cuff_pressure
0x2A1E org.bluetooth.characteristic.intermediate_temperature
Irradiance 0x2A77 org.bluetooth.characteristic.irradiance Language 0x2AA2
org.bluetooth.characteristic.language
Last Name 0x2A90 org.bluetooth.characteristic.last_name
Latitude 0x2AAE org.bluetooth.characteristic.latitude
LN Control Point 0x2A6B org.bluetooth.characteristic.ln_control_point
LN Feature 0x2A6A org.bluetooth.characteristic.ln_feature
Local East Coordinate 0x2AB1
org.bluetooth.characteristic.local_east_coordinate
Local North 0x2AB0 org.bluetooth.characteristic.local_north_coordinate
Coordinate
Local Time Information 0x2A0F
org.bluetooth.characteristic.local_time_information
Location and Speed 0x2A67 org.bluetooth.characteristic.location_and_speed
Location Name 0x2AB5 org.bluetooth.characteristic.location_name
Longitude 0x2AAF org.bluetooth.characteristic.longitude
Magnetic Declination 0x2A2C org.bluetooth.characteristic.magnetic_declination
Magnetic Flux Density 0x2AA0
org.bluetooth.characteristic.magnetic_flux_density_2D- 2D
Magnetic Flux Density 0x2AA1
org.bluetooth.characteristic.magnetic_flux_density_3D- 3D
Manufacturer Name 0x2A29
org.bluetooth.characteristic.manufacturer_name_stringString
Maximum
org.bluetooth.characteristic.maximum_recommended_heart_raRecommended Heart
0x2A91 teRate
Measurement Interval 0x2A21
org.bluetooth.characteristic.measurement_interval
Model Number String 0x2A24
org.bluetooth.characteristic.model_number_string
Navigation 0x2A68 org.bluetooth.characteristic.navigation
New Alert 0x2A46 org.bluetooth.characteristic.new_alert
Object Action Control Point
0x2AC5 org.bluetooth.characteristic.object_action_control_point
Object Changed 0x2AC8 org.bluetooth.characteristic.object_changed
Object First-Created 0x2AC1 org.bluetooth.characteristic.object_first_created
Object ID 0x2AC3 org.bluetooth.characteristic.object_id
Object Last-Modified 0x2AC2 org.bluetooth.characteristic.object_last_modified
Object List Control Point
0x2AC6 org.bluetooth.characteristic.object_list_control_point
Object List Filter 0x2AC7 org.bluetooth.characteristic.object_list_filter
Object Name 0x2ABE org.bluetooth.characteristic.object_name
Object Properties 0x2AC4 org.bluetooth.characteristic.object_properties
Object Size 0x2AC0 org.bluetooth.characteristic.object_size
Object Type 0x2ABF org.bluetooth.characteristic.object_type
OTS Feature 0x2ABD org.bluetooth.characteristic.ots_feature
0x2A04
org.bluetooth.characteristic.gap.peripheral_preferred_connecti on_parameters
0x2A02 org.bluetooth.characteristic.gap.peripheral_privacy_flag
0x2A5F org.bluetooth.characteristic.plx_continuous_measurement
PLX Features 0x2A60 org.bluetooth.characteristic.plx_features
PLX Spot-Check Measurement 0x2A5E
org.bluetooth.characteristic.plx_spot_check_measurement
PnP ID 0x2A50 org.bluetooth.characteristic.pnp_id
Pollen Concentration 0x2A75 org.bluetooth.characteristic.pollen_concentration
Position Quality 0x2A69 org.bluetooth.characteristic.position_quality
Pressure 0x2A6D org.bluetooth.characteristic.pressure
Protocol Mode 0x2A4E org.bluetooth.characteristic.protocol_mode
Rainfall 0x2A78 org.bluetooth.characteristic.rainfall
Reconnection Address 0x2A03
org.bluetooth.characteristic.gap.reconnection_address
Record Access Control Point
0x2A52 org.bluetooth.characteristic.record_access_control_point
Reference Time Information 0x2A14
org.bluetooth.characteristic.reference_time_information
Report 0x2A4D org.bluetooth.characteristic.report
Report Map 0x2A4B org.bluetooth.characteristic.report_map Resolvable Private
0x2AC9 org.bluetooth.characteristic.resolvable_private_address_onlyAddress Only
Resting Heart Rate 0x2A92 org.bluetooth.characteristic.resting_heart_rate
Ringer Control Point 0x2A40 org.bluetooth.characteristic.ringer_control_point
Ringer Setting 0x2A41 org.bluetooth.characteristic.ringer_setting
RSC Feature 0x2A54 org.bluetooth.characteristic.rsc_feature
RSC Measurement 0x2A53 org.bluetooth.characteristic.rsc_measurement
SC Control Point 0x2A55 org.bluetooth.characteristic.sc_control_point
Scan Interval Window 0x2A4F
org.bluetooth.characteristic.scan_interval_window
Scan Refresh 0x2A31 org.bluetooth.characteristic.scan_refresh
Sensor Location 0x2A5D org.blueooth.characteristic.sensor_location
Serial Number String 0x2A25 org.bluetooth.characteristic.serial_number_string
Service Changed 0x2A05 org.bluetooth.characteristic.gatt.service_changed
Software Revision 0x2A28 org.bluetooth.characteristic.software_revision_string
String
Sport Type for Aerobic
org.bluetooth.characteristic.sport_type_for_aerobic_and_anaerand Anaerobic 0x2A93
obic_thresholdsThresholds
Supported New Alert 0x2A47
org.bluetooth.characteristic.supported_new_alert_categoryCategory
Supported Unread 0x2A48
org.bluetooth.characteristic.supported_unread_alert_categoryAlert Category
System ID 0x2A23 org.bluetooth.characteristic.system_id
TDS Control Point 0x2ABC org.bluetooth.characteristic.tds_control_point
Temperature 0x2A6E org.bluetooth.characteristic.temperature
Temperature Measurement 0x2A1C
org.bluetooth.characteristic.temperature_measurement
Temperature Type 0x2A1D org.bluetooth.characteristic.temperature_type
Three Zone Heart Rate Limits 0x2A94
org.bluetooth.characteristic.three_zone_heart_rate_limits
Time Accuracy 0x2A12 org.bluetooth.characteristic.time_accuracy
Time Source 0x2A13 org.bluetooth.characteristic.time_source
Time Update Control Point
0x2A16 org.bluetooth.characteristic.time_update_control_point
Time Update State 0x2A17 org.bluetooth.characteristic.time_update_state
Time with DST 0x2A11 org.bluetooth.characteristic.time_with_dst
Time Zone 0x2A0E org.bluetooth.characteristic.time_zone
True Wind Direction 0x2A71 org.bluetooth.characteristic.true_wind_direction
True Wind Speed 0x2A70 org.bluetooth.characteristic.true_wind_speed
Two Zone Heart Rate Limit
0x2A95 org.bluetooth.characteristic.two_zone_heart_rate_limit
Tx Power Level 0x2A07 org.bluetooth.characteristic.tx_power_level
Uncertainty 0x2AB4 org.bluetooth.characteristic.uncertainty
Unread Alert Status 0x2A45 org.bluetooth.characteristic.unread_alert_status
URI 0x2AB6 org.bluetooth.characteristic.uri
User Control Point 0x2A9F org.bluetooth.characteristic.user_control_point
User Index 0x2A9A org.bluetooth.characteristic.user_index
UV Index 0x2A76 org.bluetooth.characteristic.uv_index
VO2 Max 0x2A96 org.bluetooth.characteristic.vo2_max
Waist Circumference 0x2A97 org.bluetooth.characteristic.waist_circumference
Weight 0x2A98 org.bluetooth.characteristic.weight
Weight Measurement 0x2A9D org.bluetooth.characteristic.weight_measurement
Weight Scale Feature 0x2A9E org.bluetooth.characteristic.weight_scale_feature
Wind Chill 0x2A79 org.bluetooth.characteristic.wind_chill
This is a non-exhaustive list of companies. A full list and updated can be found
on the Bluetooth SIG website.
Table VII-1. Company Identifiers
7 0x0007 Lucent
8 0x0008 Motorola
Bluetooth Low Energy (BLE) - A low power, short range wireless protocol
used on micro electronics.
Broadcast - A feature of Bluetooth Low Energy where a Peripheral outputs a
name and other specific data about a itself
Central - A Bluetooth Low Energy device that can connect to several
Peripherals.
Channel - A finely-tuned radio frequency used for Broadcasting or data
transmission.
Characteristic - A port or data endpoint where data can be read or written.
Descriptor - A feature of a Characteristic that allows for some sort of data
interaction, such as Read, Write, or Notify.
E0 - The encryption algorithm built into Bluetooth Low Energy.
essential truth that defines a company. Tony’s infinite curiosity compels him to
want to open up and learn about everything he touches, and his excitement
compels him to share what he learns with others.
His passion for inventing led him to start a company that uses brain imaging to
quantify meditation and to predict seizures, a company acquired $1.5m in
funding and was incubated in San Francisco where he currently resides.
Those same passions have led him on some adventures as well, including living
in a Greek monastery with orthodox monks and to tagging along with a gypsy in
Spain to learn to play flamenco guitar.
371
(This page intentionally left blank)
ccclxxii ccclxxiii
About this Book
This book is a practical guide to programming Bluetooth Low Energy for iPhone
and iPad.
In this book, you will learn the basics of how to program an iOS device to
communicate with any Central or Peripheral device over Bluetooth Low Energy.
Each chapter of the book builds on the previous one, culminating in three
projects:
Skill Level
This book is excellent for anyone who has basic or advanced knowledge of iOS
programming in SWIFT.
Other Books in this Series
If you are interested in programming other Bluetooth Low Energy Devices,
please check out the other books in this series or visit
bluetoothlowenergybooks.com:
Bluetooth Low Energy Programming in Android Java
Tony Gaitatzis, 2017
ISBN: 978-1-7751280-1-4
Bluetooth Low Energy Programming in Arduino C++
Tony Gaitatzis, 2017
ISBN: 978-1-7751280-2-1
Bluetooth Low Energy Programming in C++ for nRF51822 and nRF52832
Tony Gaitatzis, 2017
ISBN: 978-1-7751280-3-8 (This page intentionally left blank)
ccclxxvi