Skip to content

Instantly share code, notes, and snippets.

@stonehippo
Last active June 25, 2021 21:39
Show Gist options
  • Save stonehippo/ffbed2f32e1813f4019ecedd22062636 to your computer and use it in GitHub Desktop.
Save stonehippo/ffbed2f32e1813f4019ecedd22062636 to your computer and use it in GitHub Desktop.
Some example Arduino IDE code showing how to do various things with Adafruit nrF52 Feather BLE

Adafruit nRF52 BLE Example

I made this example file for a coworker. It illustrates a few things:

  • Setting up a custom service
  • Setting up custom characteristics on that service
  • Connecting to the littlefs file system to read and write files for persistance
  • Setting manufacturer data in the BLE scan response
  • using littlefs-persisted data to maintain characteristic state between device power cycles
  • very basic connect/disconnect event callbacks
  • multiple characteristics for "slots"

I have updated this gist a few times, largely in response to changes in the Adafruit nRF52 Arduino library. As of July 24, 2019, this code requires >=0.11.1 of the Adafruit Library (which introduced some breaking changes in the API).

This example is available via the MIT license, so you can reuse or modify it. See LICENSE.txt in this gist for details.

Converting this example for use with PlatformIO

I use PlatformIO for most of my embedded dev these days, rather than the Arduino IDE. I find it to be a generally better experience. But I hacked this example together originally for use with Arduino IDE, since that's more common for hobbyist developers.

If you're like me, and you want to use this example with PlatformIO, there are a couple of things you'll want to do. First is to use pio init to create a new project. Once you've got that, add this to the project's platformio.ini:

[env:adafruit_feather_nrf52832]
platform = nordicnrf52
board = adafruit_feather_nrf52832
framework = arduino
upload_protocol = nrfutil

Note that is is using the current, upstream version of the PIO nordicnrf52 platform (which was needed as of 7/24/19, because it includes the Adafruit BSP version 0.11.1).

That will set up the device target and uses the develop version of the Nordic nRF52 board support in PlatformIO (required as of 3/3/19, as the Adafruit Feather support hasn't been included in a release yet). This will use the current production release of the Adafruit Feather nRF52 board support from PlatformIO. Note: Release 3.4.0 of platformio-nordicnrf52 has a bug that will attempt to force the inclusion of the nrfjprog utility. On most platforms, this is not an issue. I run most of my PlatformIO code from a Raspberry Pi, however, and nrfjprog isn't available for ARM-based systems (I rely on adafruit-nrfutil instead).

PlatformIO can handle .ino files, but I prefer to use more standard C++ .cpp files for Arduino projects. So the next step is to copy the contents of adafruit_nrf52_example.ino into src/main.cpp in the project. You will need to include the Arduino framework, too, so append the following above the other #include statements:

#include <Arduino.h>

Lastly, because this is real C++ and not the Arduino IDE, the functions used in the code need to get defined, so create include/main.h and put this in it:

#include <Arduino.h>

bool getUniqueID();
void setUniqueID();
void writeState();
uint8_t readState();
void setState();
void connectCallback(uint16_t handle);
void disconnectCallback(uint16_t handle, uint8_t reason);

You'll need to make sure main.h is used, so append this after the other includes at the top of main.cpp:

#include "main.h"

With this done, everything should be set and you can get all of the needed dependencies and build the example with:

$ pio run

Assuming that works (I've tested it, so it should), you can deploy this code to an Adafruit nRF52 Feather with

$ pio run -t upload
/*
* Adafruit nRF52 Feather Example
* Copyright (c) 2018-2019 George White <stonehippo@gmail.com>
*
* MIT LICENSE, see LICENSE.txt at https://gist.github.com/stonehippo/ffbed2f32e1813f4019ecedd22062636
*
* I made this example file for a coworker. It illustrates a few things:
*
* - Setting up a custom service
* - Setting up custom characteristics on that service
* - Connecting to the littlefs file system to read and write files for persistance
* - Setting manufacturer data in the BLE scan response
* - using littlefs-persisted data to maintain characteristic state between device power cycles
* - very basic connect/disconnect event callbacks
* - multiple characteristics for "slots"
*
* This example is dependent on the Adafruit nRF52 Feather board support; because it uses littlefs
* to persist data on device, it requires at least version 0.11.1. For more info see
* https://github.com/adafruit/Adafruit_nRF52_Arduino
*/
#include <bluefruit.h>
#include <Adafruit_LittleFS.h>
#include <InternalFileSystem.h>
using namespace Adafruit_LittleFS_Namespace;
const uint8_t SERVICE_UUID[16] = {0x3,0x4E,0x53,0xD7,0xD1,0xC4,0x3C,0x8A,0x60,0x40,0x85,0xC8,0xE,0xA9,0x44,0x63};
const uint8_t CHARACTERISTIC_UUID[16] = {0x1,0x0,0x0,0x0,0xD1,0xC4,0x3C,0x8A,0x60,0x40,0x85,0xC8,0xE,0xA9,0x44,0x63};
const uint8_t MANUFACTURER_DATA_UUID[16] = {0x2,0x0,0x0,0x0,0xD1,0xC4,0x3C,0x8A,0x60,0x40,0x85,0xC8,0xE,0xA9,0x44,0x63};
const uint8_t ADDRESS_ONE[16] = {0x3,0x0,0x0,0x0,0xD1,0xC4,0x3C,0x8A,0x60,0x40,0x85,0xC8,0xE,0xA9,0x44,0x63};
const uint8_t ADDRESS_TWO[16] = {0x4,0x0,0x0,0x0,0xD1,0xC4,0x3C,0x8A,0x60,0x40,0x85,0xC8,0xE,0xA9,0x44,0x63};
const uint8_t ADDRESS_THREE[16] = {0x5,0x0,0x0,0x0,0xD1,0xC4,0x3C,0x8A,0x60,0x40,0x85,0xC8,0xE,0xA9,0x44,0x63};
const uint8_t ADDRESS_FOUR[16] = {0x6,0x0,0x0,0x0,0xD1,0xC4,0x3C,0x8A,0x60,0x40,0x85,0xC8,0xE,0xA9,0x44,0x63};
const uint8_t ADDRESS_FIVE[16] = {0x7,0x0,0x0,0x0,0xD1,0xC4,0x3C,0x8A,0x60,0x40,0x85,0xC8,0xE,0xA9,0x44,0x63};
BLEService single = BLEService(SERVICE_UUID);
BLECharacteristic data = BLECharacteristic(CHARACTERISTIC_UUID);
BLECharacteristic manData = BLECharacteristic(MANUFACTURER_DATA_UUID);
BLECharacteristic addressOne = BLECharacteristic(ADDRESS_ONE);
BLECharacteristic addressTwo = BLECharacteristic(ADDRESS_TWO);
BLECharacteristic addressThree = BLECharacteristic(ADDRESS_THREE);
BLECharacteristic addressFour = BLECharacteristic(ADDRESS_FOUR);
BLECharacteristic addressFive = BLECharacteristic(ADDRESS_FIVE);
uint8_t state = 1;
uint8_t randomByte() {
return random(255);
}
char uniqueID[4];
// Managing state persistance in LittleFS file system
#define STATE_FILENAME "/state.txt"
#define ID_FILENAME "/id.txt"
File file(InternalFS);
void setup() {
Serial.begin(115200);
char a1[3] = { 0xAF, 0x07, 0xCF };
char a2[3] = { 0xAF, 0x24, 0xAF };
// seed the PRNG with random noise from measuring an analog input
randomSeed(analogRead(A5));
Bluefruit.begin();
Bluefruit.setName("Adafruit nrf52 Demo");
// start the file system lib and see if we have anything persisted
InternalFS.begin();
//InternalFS.format(true); // Uncomment to wipe out all persisted data, including state and unique ID
setState();
// set the unique ID for this device
if (!getUniqueID()) {
setUniqueID();
}
Bluefruit.Periph.setConnectCallback(connectCallback);
Bluefruit.Periph.setDisconnectCallback(disconnectCallback);
// Adverstising
Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
Bluefruit.Advertising.addService(single);
Bluefruit.ScanResponse.addName();
Bluefruit.ScanResponse.addManufacturerData(uniqueID, sizeof(uniqueID) - 1);
Bluefruit.Advertising.restartOnDisconnect(true);
Bluefruit.Advertising.setInterval(32, 244);
Bluefruit.Advertising.setFastTimeout(30);
Bluefruit.Advertising.start(0);
// Service
single.begin();
data.setProperties(CHR_PROPS_READ | CHR_PROPS_WRITE);
data.setPermission(SECMODE_OPEN, SECMODE_OPEN);
data.setUserDescriptor("Test Data");
data.setFixedLen(1);
data.begin();
// set the initial value
data.write8(state);
manData.setProperties(CHR_PROPS_READ);
manData.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS);
manData.setUserDescriptor("Unique Device ID");
manData.setMaxLen(8);
manData.begin();
manData.write(uniqueID, sizeof(uniqueID) - 1); // truncate the last bit, since it's there to make it a string
addressOne.setProperties(CHR_PROPS_READ | CHR_PROPS_WRITE);
addressOne.setPermission(SECMODE_OPEN, SECMODE_OPEN);
addressOne.setMaxLen(3);
addressOne.begin();
addressOne.write(a1, sizeof(a1));// Set this to a fake address for fun
addressTwo.setProperties(CHR_PROPS_READ | CHR_PROPS_WRITE);
addressTwo.setPermission(SECMODE_OPEN, SECMODE_OPEN);
addressTwo.setMaxLen(3);
addressTwo.begin();
addressTwo.write(a2, sizeof(a2));
addressThree.setProperties(CHR_PROPS_READ | CHR_PROPS_WRITE);
addressThree.setPermission(SECMODE_OPEN, SECMODE_OPEN);
addressThree.setMaxLen(3);
addressThree.begin();
addressFour.setProperties(CHR_PROPS_READ | CHR_PROPS_WRITE);
addressFour.setPermission(SECMODE_OPEN, SECMODE_OPEN);
addressFour.setMaxLen(3);
addressFour.begin();
addressFive.setProperties(CHR_PROPS_READ | CHR_PROPS_WRITE);
addressFive.setPermission(SECMODE_OPEN, SECMODE_OPEN);
addressFive.setMaxLen(3);
addressFive.begin();
}
void loop() {
// do a primitive check to see if we should report that the value
// has changed as an example of reading the value
uint8_t value = data.read8();
if (state != value) {
Serial.print("State has changed: ");
Serial.print(state);
Serial.print(" -> ");
Serial.println(value);
state = value;
writeState();
readState();
}
}
bool getUniqueID() {
file.open(ID_FILENAME, FILE_O_READ);
if(file) {
uint32_t readLen;
char buffer[64] = { 0 };
readLen = file.read(buffer, sizeof(buffer));
buffer[readLen] = 0;
Serial.println("Got unique ID from persistance");
Serial.println(buffer);
file.close();
return true;
}
return false;
}
void setUniqueID() {
if (file.open(ID_FILENAME, FILE_O_WRITE)) {
uniqueID[0] = randomByte();
uniqueID[1] = randomByte();
uniqueID[2] = randomByte();
uniqueID[3] = 0;
file.write(uniqueID, sizeof(uniqueID));
file.close();
Serial.println("set a new unique ID in persistance");
}
}
// Write the state to the local file system
void writeState() {
if(file.open(STATE_FILENAME, FILE_O_WRITE)) {
char buffer[12];
itoa(state, buffer, 10);
file.write(buffer, sizeof(buffer));
file.close();
}
}
// Read the state from the local file system
uint8_t readState() {
uint8_t value = 1; // set the default to return
file.open(STATE_FILENAME, FILE_O_READ);
if (file) {
Serial.println("Value from " STATE_FILENAME);
uint32_t readLen;
char buffer[64] = { 0 }; // buffer starts as an empty char array (C string)
readLen = file.read(buffer, sizeof(buffer));
buffer[readLen] = 0; // drop any last character and make sure the buffer contains a C string
value = atoi(buffer); // convert the string in the file to a number
Serial.println(value);
file.close();
} else {
Serial.println("State file does not yet exist");
}
return value;
}
// take whatever number we read from the file and store it in current state
void setState() {
state = readState();
}
void connectCallback(uint16_t handle) {
// As of Adafruit nRF52 Arduino 0.10.1, use BLEConnection
BLEConnection* connection = Bluefruit.Connection(handle);
// handle checks for running sessions and other reconnection details here
char central[32] = { 0 };
connection->getPeerName(central, sizeof(central));
Serial.print("Connected to ");
Serial.println(central);
}
void disconnectCallback(uint16_t handle, uint8_t reason) {
// handle anything disconnection details here
Serial.println("Disconnected, resuming advertising");
}
The MIT License (MIT)
Copyright (c) 2018-2019 George White <stonehippo@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@Protonerd
Copy link

I'm not sure I get it right: above you write that the sketch uses the littleFS, but it seems to me you initialize the InternalFS instead. Do you have an example for the litteFS usage as well?

@stonehippo
Copy link
Author

stonehippo commented Oct 27, 2019

@Protonerd InternalFileSystem is dependent on LittleFS. Honestly, the explicit include is redundant, since InternalFileSystem brings in its dependencies, so you could drop it and things would work the same.

The InternalFileSystem API is a higher-level abstraction and it’s easier to work with than dealing with LittleFS directly. However, if you want to do that, the @adafruit repo has a decent example in the README: https://github.com/adafruit/Adafruit_nRF52_Arduino/blob/b928d9b43eecef4f0f3d6779930c5d60cf0b050f/libraries/Adafruit_LittleFS/src/littlefs/README.md

Hope that helps.

@Protonerd
Copy link

Thanks stonehippo for the clarification, in fact it took me another 5 mins to figure it out myself, I also do not fully understand why they choose to do it in 3 layers, as they made a wrapper already around littleFS, so InternalFileSystem is in fact the 3rd wrapper. But on the other hand side this is by far the most user friendly persistent storage for the Nordic nRF52. I'm grateful for Adafruit to provide libs for this platform, as Nordic is absolutely not interested in the Arduino community...

@lewisrichardson2103
Copy link

This is a great example, I have found it really useful as there is not much on the web for this.
Do you happen to know if there is a way to clear the content of a file? or is it easier to delete and re-create the file?

@stonehippo
Copy link
Author

@lewisrichardson2103 I'm glad this was helpful for you!

Based on a quick check of the library source, I think you could call truncate() on a file to empty it out prior to calling write(). You can see the whole API definition here: https://github.com/adafruit/Adafruit_nRF52_Arduino/blob/9a7034bddb9fb50a80f3d4a35b7bebf1762a31ee/libraries/Adafruit_LittleFS/src/Adafruit_LittleFS_File.h

@lewisrichardson2103
Copy link

@lewisrichardson2103 I'm glad this was helpful for you!

Based on a quick check of the library source, I think you could call truncate() on a file to empty it out prior to calling write(). You can see the whole API definition here: https://github.com/adafruit/Adafruit_nRF52_Arduino/blob/9a7034bddb9fb50a80f3d4a35b7bebf1762a31ee/libraries/Adafruit_LittleFS/src/Adafruit_LittleFS_File.h

Excellent that should do the trick! I must have missed that when I was looking through the API. Thanks again!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment