An IoT Triad Demo, Part I – Device (nRF8001 + Arduino)

Introduction
Internet Of Things (IoT) implementations usually consist of three parts – the device itself, software that interacts with the device (which usually runs on a mobile platform), and a cloud part that can serve as an intermediary, which may host web apps for controlling the device or dashboards for device data. In this three-part article, I demonstrate an “IoT triad” by developing a simple Bluetooth Low Energy (BLE) device, a cross platform mobile application, and data visualization on the cloud. In this first installment of the project, I will focus on the device part.
The Device
Our IoT device consists of an Adafruit nRF8001 breakout board controlled by an Arduino Pro Mini (clone). It measures the ambient temperature from an LM35 sensor connected to analog pin of the Arduino, and sends out this information to connected BLE peers. In addition, it also broadcasts a (fake) battery level to listeners. Here is how we hook up the device:

The connections above are same as that in the Adafruit nRF8001 article. (Note that we’re using a 5V Arduino.) REQ connects to the SPI Chip Select pin, which is set as Digital 10 here. RDY is the interrupt out from the nRF8001, which is connected to Digital 2 (It needs to be connected to an interrupt capable pin). RST connects to Digital 9, and is used to restart the nRF8001 board when the Arduino starts up.
Firmware
Now, we need to develop the firmware for our IoT device. The BLE magic happens in the nRF8001, and the Arduino acts as a controller for this chip. The communication between the nRF8001 and the Arduino happens via something called the Application Controller Interface (ACI). This scheme is shown below.

Note that there is one extra connection above than the usual SPI scheme. (This is because the nRF8001 doesn’t act as a pure SPI slave device.) The controller and the nRF8001 communicate in a bidirectional scheme. Information going from the controller to the nRF8001 are in the form of system commands and data commands. Information going the other way is in the form of system events and data events. For the gory details, you can (and you must!) read the 161 page nRF8001 data sheet. Luckily, we don’t have to code everything up from the data sheet ourselves. Nordic has provided an excellent SDK for the Arduino with great examples which we can build upon.
nRF8001 Configuration
Before we can use the nRF8001, we need to configure it. Nordic provides a Windows-only (why Oh why?!) nRFGo utility that can be used to generate the files needed to configure the GATT and GAP settings as well as other hardware settings for the nRF8001. If you are unfamiliar with these BLE concepts, I recommend that you start reading Getting Started with Bluetooth Low Energy by Kevin Townsend et al. (BLE has a tonne of acronymns and concepts, and if you’re like me, it will take multiple readings for the dust to settle.)
To get started, we have to first decide what kind of services our BLE device is going to provide. In our case, we want to send out temperature out (measured by the Arduino using the LM35), as well as a fake battery level. The BLE specification describes a number of standard Services, and we’re going to make use of a couple of them. For sending out the temperature, we will make use of the Health Thermometer Profile. This service specifies a number of characteristics, but we’ll make use of only one that we’re interested in – Temperature Measurement. For battery level, we’ll make use of the Battery Service profile, and the characteristic of interest in this case is Battery Level.
Nordic uses the concept of Service Pipes for the nRF8001 ACI. Each service pipe points a unique charactacteristic – for example, the Battery Level as in our case. The pipe also defines the direction of data transfer, and a bunch of other properties. When we configure the nRF8001 using nRFGo, it will create the definitions required to create these pipes in services.h.
The way we use nRFGo is as follows:
- Select File->New->nRF8001.
- Add GATT services to device by dragging and dropping from “Service Templates” tab onto the tab on the left.
- Modify GAP settings.
- Go to nRFSetup menu, generate source files and choose the option to generate just the .h file.
- Copy the generated services.h file to our Arduino project directory.
Here is what the GATT settings looks like:

For the Health Thermometer profile, we have only included the Temperature Measurement characteristic. In its properties, we have checked the Indicate option, which means the following (from nRF8001 data sheet):
- Update Server and send an indication of the update to the Client (Peer device).
- Peer device acknowledges a successful reception of the indication.
- nRF8001 generates DataAckEvent
We also have checked the “Use characteristic presentation format” option, and set the value to be a 4 byte FLOAT with an initial value of 22. For the Battery Service profile, we have enabled “broadcast” and “set pipe” for the Battery Level characteristic. We’ve also set the characteristic format at an unsigned 8 bit integer with an initial value of 11.
Now let’s look at the GAP settings:

In the GAP Settings tab, I have set the device name as IoT electronut. I’ve also set values in the ACI connect/bond/broadcast tabs. This is something you can play with – I am yet to get too deep into these. To see and modify the settings I used above, simply load the arduino_nrf8001_iot.xml file into nRFGo from my github link for the project.
At the end of the above process, we end up with a services.h file that we will use in our project. (nRFGo also generates a services_lock.h, which is to be only used for production, since that will write the settings permanently into the non-volatile RAM of the nRF8001.) If you look inside the services.h file, you will see stuff like:
/* Service: Battery - Characteristic: Battery Level - Pipe: BROADCAST */
#define PIPE_BATTERY_BATTERY_LEVEL_BROADCAST 1
#define PIPE_BATTERY_BATTERY_LEVEL_BROADCAST_MAX_SIZE 1
/* Service: Battery - Characteristic: Battery Level - Pipe: SET */
#define PIPE_BATTERY_BATTERY_LEVEL_SET 2
#define PIPE_BATTERY_BATTERY_LEVEL_SET_MAX_SIZE 1
/* Service: Health Thermometer - Characteristic: Temperature Measurement - Pipe: TX_ACK */
#define PIPE_HEALTH_THERMOMETER_TEMPERATURE_MEASUREMENT_TX_ACK 3
#define PIPE_HEALTH_THERMOMETER_TEMPERATURE_MEASUREMENT_TX_ACK_MAX_SIZE 4
The above three pipes match our settings in the GATT tab in nRFGo. These pipe handles will be used in our code. The services.h file also contains the initialization code containing all our settings. This looks something like:
#define SETUP_MESSAGES_CONTENT {\
{0x00,\
{\
0x07,0x06,0x00,0x00,0x03,0x02,0x42,0x07,\
},\
},\
{0x00,\
{\
0x1f,0x06,0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x03,0x01,0x01,0x00,0x00,0x06,0x00,0x00,\
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,\
},\
},\
...
To be clear, you don’t absolutely require nRFGo to make all this work. As long as you are able to send the correct initialization commands to nRF8001 via SPI, it will work. But as you can imagine, coding this up manually would be horribly painful.
Here is the Arduino code that uses the information in services.h to setup the nRF8001:
// Point ACI data structures to the the setup data that
// the nRFgo studio generated for the nRF8001
aci_state.aci_setup_info.services_pipe_type_mapping =
&services_pipe_type_mapping[0];
aci_state.aci_setup_info.number_of_pipes = NUMBER_OF_PIPES;
aci_state.aci_setup_info.setup_msgs = (hal_aci_data_t*) setup_msgs;
aci_state.aci_setup_info.num_setup_msgs = NB_SETUP_MESSAGES;
// set uC connections
aci_state.aci_pins.board_name = BOARD_DEFAULT;
aci_state.aci_pins.reqn_pin = 10;
aci_state.aci_pins.rdyn_pin = 2;
aci_state.aci_pins.mosi_pin = MOSI;
aci_state.aci_pins.miso_pin = MISO;
aci_state.aci_pins.sck_pin = SCK;
aci_state.aci_pins.spi_clock_divider = SPI_CLOCK_DIV8;
aci_state.aci_pins.reset_pin = 9;
aci_state.aci_pins.active_pin = UNUSED;
aci_state.aci_pins.optional_chip_sel_pin = UNUSED;
aci_state.aci_pins.interface_is_interrupt = false;
aci_state.aci_pins.interrupt_number = 1;
// this call inititualizes the nRF8001 with our settings
lib_aci_init(&aci_state, false);
In the code above, we can see how the Nordic ACI library data structures are set up according to the configuration we created in the services.h file. The code also sets the microcontroller pins used to communicate with the nRF8001, and finally calls lib_aci_init() to initialize it.
nRF8001 Flow Control
The nRF8001 is state machine with the following four modes of operation: Sleep, Setup, Active, and Test. The following graphic illustrates how it switches between these modes.

Here is the boilerplate code from the Nordic BLE SDK, which manages the event loop in the nRF8001:
void aci_loop()
{
static bool setup_required = false;
// We enter the if statement only when there is a ACI event
// available to be processed
if (lib_aci_event_get(&aci_state, &aci_data))
{
aci_evt_t * aci_evt;
aci_evt = &aci_data.evt;
switch(aci_evt->evt_opcode) {
case ACI_EVT_DEVICE_STARTED:
{
aci_state.data_credit_available =
aci_evt->params.device_started.credit_available;
switch(aci_evt->params.device_started.device_mode)
{
case ACI_DEVICE_SETUP:
{
Serial.println(F("Evt Device Started: Setup"));
aci_state.device_state = ACI_DEVICE_SETUP;
setup_required = true;
}
break;
case ACI_DEVICE_STANDBY:
{
aci_state.device_state = ACI_DEVICE_STANDBY;
// sleep_to_wakeup_timeout = 30;
Serial.println(F("Evt Device Started: Standby"));
if (aci_evt->params.device_started.hw_error) {
//Magic number used to make sure the HW error
//event is handled correctly.
delay(20);
}
else
{
lib_aci_connect(30/* in seconds */,
0x0100 /* advertising interval 100ms*/);
Serial.println(F("Advertising started"));
}
}
break;
}
}
break; // case ACI_EVT_DEVICE_STARTED:
case ACI_EVT_CMD_RSP:
{
//If an ACI command response event comes with an error -> stop
if (ACI_STATUS_SUCCESS != aci_evt->params.cmd_rsp.cmd_status ) {
// ACI ReadDynamicData and ACI WriteDynamicData
// will have status codes of
// TRANSACTION_CONTINUE and TRANSACTION_COMPLETE
// all other ACI commands will have status code of
// ACI_STATUS_SCUCCESS for a successful command
Serial.print(F("ACI Status of ACI Evt Cmd Rsp 0x"));
Serial.println(aci_evt->params.cmd_rsp.cmd_status, HEX);
Serial.print(F("ACI Command 0x"));
Serial.println(aci_evt->params.cmd_rsp.cmd_opcode, HEX);
Serial.println(F("Evt Cmd respone: Error. "
"Arduino is in an while(1); loop"));
while (1);
}
else
{
// print command
Serial.print(F("ACI Command 0x"));
Serial.println(aci_evt->params.cmd_rsp.cmd_opcode, HEX);
}
}
break;
case ACI_EVT_CONNECTED:
{
// The nRF8001 is now connected to the peer device.
Serial.println(F("Evt Connected"));
}
break;
case ACI_EVT_DATA_CREDIT:
{
Serial.println(F("Evt Credit: Peer Radio acked our send"));
/** Bluetooth Radio ack received from the peer radio for
the data packet sent. This also signals that the
buffer used by the nRF8001 for the data packet is
available again. We need to wait for the Confirmation
from the peer GATT client for the data packet sent.
The confirmation is the ack from the peer GATT client
is sent as a ACI_EVT_DATA_ACK. */
}
break;
case ACI_EVT_DISCONNECTED:
{
// Advertise again if the advertising timed out.
if(ACI_STATUS_ERROR_ADVT_TIMEOUT ==
aci_evt->params.disconnected.aci_status) {
Serial.println(F("Evt Disconnected -> Advertising timed out"));
Serial.println(F("nRF8001 going to sleep"));
lib_aci_sleep();
aci_state.device_state = ACI_DEVICE_SLEEP;
}
else
{
Serial.println(F("Evt Disconnected -> Link lost."));
lib_aci_connect(30/* in seconds */,
0x0050 /* advertising interval 50ms*/);
Serial.println(F("Advertising started"));
}
}
break;
case ACI_EVT_PIPE_STATUS:
{
Serial.println(F("Evt Pipe Status"));
// check if the peer has subscribed to the
}
break;
case ACI_EVT_PIPE_ERROR:
{
// See the appendix in the nRF8001
// Product Specication for details on the error codes
Serial.print(F("ACI Evt Pipe Error: Pipe #:"));
Serial.print(aci_evt->params.pipe_error.pipe_number, DEC);
Serial.print(F(" Pipe Error Code: 0x"));
Serial.println(aci_evt->params.pipe_error.error_code, HEX);
// Increment the credit available as the data packet was not sent.
// The pipe error also represents the Attribute protocol
// Error Response sent from the peer and that should not be counted
//for the credit.
if (ACI_STATUS_ERROR_PEER_ATT_ERROR !=
aci_evt->params.pipe_error.error_code) {
aci_state.data_credit_available++;
}
}
break;
case ACI_EVT_DATA_ACK:
{
Serial.println(F("Attribute protocol ACK for"));
}
break;
case ACI_EVT_HW_ERROR:
{
Serial.println(F("HW error: "));
Serial.println(aci_evt->params.hw_error.line_num, DEC);
for(uint8_t counter = 0; counter <= (aci_evt->len - 3); counter++)
{
Serial.write(aci_evt->params.hw_error.file_name[counter]);
}
Serial.println();
lib_aci_connect(30/* in seconds */,
0x0100 /* advertising interval 100ms*/);
Serial.println(F("Advertising started"));
}
break;
default:
{
Serial.print(F("Evt Opcode 0x"));
Serial.print(aci_evt->evt_opcode, HEX);
Serial.println(F(" unhandled"));
}
break;
}
}
else
{
// No event in the ACI Event queue
}
/* setup_required is set to true when the device starts up and
enters setup mode.
* It indicates that do_aci_setup() should be
called. The flag should be cleared if
* do_aci_setup() returns ACI_STATUS_TRANSACTION_COMPLETE. */
if(setup_required)
{
if (SETUP_SUCCESS == do_aci_setup(&aci_state))
{
setup_required = false;
}
}
}
So the aci_loop() method above handles the event loop in the nRF8001, and we need to add our code to specific event handling sections within this code. This is how we integrate aci_loop() into our Arduino code:
void loop()
{
aci_loop();
// every 5 seconds
if(millis() - lastUpdate > 5000) {
// do something!
// update time stamp
lastUpdate = millis();
}
}
Measuring & Sending Temperature
We measure temperature using the LM35 sensor, which is connected to the Analog 0 pin of the Arduino. Here is code that calculates temperature from this sensor:
// read the value from LM35.
// read 10 values for averaging.
int val = 0;
for(int i = 0; i < 10; i++) {
val += analogRead(lm35Pin);
delay(500);
}
// convert to temp:
// temp value is in 0-1023 range
// LM35 outputs 10mV/degree C. ie, 1 Volt => 100 degrees C
// So Temp = (avg_val/1023)*5 Volts * 100 degrees/Volt
float temp = val*50.0f/1023.0f;
Now let’s look at how the data is sent to BLE peers.
lib_aci_send_data(PIPE_HEALTH_THERMOMETER_TEMPERATURE_MEASUREMENT_TX_ACK,
(uint8_t*)&temp, 4);
Before we send the data, we need to make sure that a peer has notifications turned on. This is done in the ACI loop:
case ACI_EVT_PIPE_STATUS:
{
Serial.println(F("Evt Pipe Status"));
// check if the peer has subscribed to the
// Temperature Characteristic
if (lib_aci_is_pipe_available(&aci_state,
PIPE_HEALTH_THERMOMETER_TEMPERATURE_MEASUREMENT_TX_ACK))
{
notifyTemp = true;
}
else {
notifyTemp = false;
}
}
break;
Broadcasting Battery Level
In addition to temperature, we’re also going to send a fake battery level – just to demonstrate the concept of broadcasting. The broadcasting is set up in the event loop as follows:
case ACI_DEVICE_STANDBY:
{
aci_state.device_state = ACI_DEVICE_STANDBY;
if (!broadcastSet) {
lib_aci_open_adv_pipe(PIPE_BATTERY_BATTERY_LEVEL_BROADCAST);
Serial.println(F("Broadcasting started"));
broadcastSet = true;
}
// sleep_to_wakeup_timeout = 30;
Serial.println(F("Evt Device Started: Standby"));
if (aci_evt->params.device_started.hw_error) {
//Magic number used to make sure the HW error
//event is handled correctly.
delay(20);
}
else
{
lib_aci_connect(30/* in seconds */,
0x0100 /* advertising interval 100ms*/);
Serial.println(F("Advertising started"));
}
}
break;
Here is where the actual broadcasting is done – in loop():
if (broadcastSet) {
Serial.println(F("Setting batt level"));
uint8_t val = batt[index++ % 3];
lib_aci_set_local_data(&aci_state, PIPE_BATTERY_BATTERY_LEVEL_BROADCAST, (uint8_t*)&val, 1);
}
The battery level just cycles through an array that has values {25, 50, 75}.
Testing BLE
For testing our BLE device, I used the excellent LightBlue app on my iPhone 5. There are plenty of BLE scanner apps vailable for other platforms – make sure you pick one that can listen to BLE notifications and read advertisement data. The images below show the interaction of our device with the app.

When the app starts, we will see our BLE device listed on it, and we can see the services supported, as well as start listening for notifications.
Important Note for Debugging
When playing around with BLE, if you find that your old GATT settings are not going away in your mobile app, restart bluetooth on the phone. (This happens due to GATT caching, as I learned painfully.)
Getting the Code
All source files for this project can be found at my github page.
Conclusion
By constructing a BLE IoT contraption that transmits temperature and battery levels, we have touched on the Device part of the IoT triad. In the next part, we will look at the Mobile part – creating a cross platform mobile application that will speak to our Device.
References
- Nordic Semiconductor nRF8001 data sheet.
- Getting Started with Bluetooth Low Energy by Kevin Townsend, Carles Cufí, Akiba, & Robert Davidson. (O’Reilly Media, ISBN-13: 978-1491949511)
- Nordic nRF8001 BLE SDK.