This code is used as part of a BLE scavenger hunt game where multiple BLE beacons are scattered around an area, hidden. The user has to walk around and find them with their phone, collecting passphrases as they go from the server the beacons point to. Each beacon passes its MAC address in the query string to the server to uniqly identify it.

Dependencies:   BLE_API mbed nRF51822

Fork of BLE_URIBeacon by Bluetooth Low Energy

Intro

This code demonstrates one possible use of BLE URIBeacons. In this demo multiple beacons are loaded with the same piece of code. Each beacon will add its mac address to the query string to be sent to the server. This allows each board to be paired wtih unique information in the server database. In this instance a company ID and passphrase are associated with each beacon. This information is then displayed either in the notification tray on the phone or on the webpage when connected to.

Using it

To use the app please make sure the shortened URL in the mbed code points to a server running the server code (in this case we are providing Google App Engine codeas an example). When a board is first connected to the server the user will need assign it a company ID and a passphrase. On later connections to the server this information will be returned to the requesting device (usually a smartphone / tablet)

In short there are two phases of operation, setup and use.

Setup:

  1. make sure shortened url in mbed code points to app engine server running code
  2. load code to mbed enabled BLE board
  3. load app engine with provided code
  4. connect for first time to server via physical web app and register the device

Use:

  1. scan with physical web app
  2. uribeacon is found, companyID and passphrase are displayed in notification bar (alternatively can connect to webpage for same info)

Examples

This photo shows information from the server being shown in the notification drawer on android. There are 2 beacons shown, the first beacon is in phase 2: Use, it has been initialized on the server with a company name of 'Random company' and a passphrase of 'Wombats are awesome!!'. The other beacon is in phase 1: setup, and has not been initialized, so it displays the "please configure me" message. /media/uploads/mbedAustin/screenshot_2015-04-16-21-10-16-1-.png

Constraints

This example relies on passing along a query string '?q=" from the shortened url to the long url. Not all shortening services do this. We recommend using tiny.cc or snurl.com as they will pass along the '?q=.....' to the server. Currently the shortened URL in the example points to mbeduribeacon.appspot.com . The duration of the shortened URL is not guaranteed, so it may revert back to nothing in the future. Also the appspot server may go down in the future, thus we have provided the source code for both sides to the user.

This example also relies on the Physical Web app (available for iOS and Android). As of the date of this writing it works just fine, but since we at mbed do not control this app, this may change in the future.

Sometimes something will take a little time to buffer things, im not sure if its the google app engine or the proxy that URIBeacons are run through on the PhyscialWeb app. This means there may be up to a 20min delay between when you do the initial setup of the device and when it starts showing up correctly on your devices notification tray, but if you click through to the webpage it should work immediately after being setup.

Technical Details

This example works by forwarding the mac address of the device in the query string '?q=MACADDR' to the server. The server will return a webpage based on the mac address in the query string. Not all url shortening services do this, so we used snurl.com, because it does. The PhysicalWeb phone app will scan the beacon, ping the server, and display the MetaData (description, favicon, title, url) fields in the notification drawer and in the physical web app. This entire demo relies on the PhysicalWeb app to handle the phone side interaction.

Summary

This is a cool demo of how to use URIBeacons to facilitate connecting the physical and digital worlds. It is by no means an end all be all, but is just one way to use them. Go build something awesome!

Other things

This is the the google app engine code necessary to run this example. If you would like to run your own server you can sign up for a free app engine account and use this python code as your example code.

ServerCode.py

from google.appengine.api import users
import cgi

import webapp2

import pprint

register = {
	'test':{'companyid':'Awesomeness Inc.','passphrase':'wombats of the doom'},
}

ADD_PAGE_HTML = '''\
<html>
	<head>
		<title>mbed Powered Scavenger Hunt</title>
		<meta name="description" content="This device is not recognized. Please click to add device to database.">
	</head>
  <body>
  	<h2>This device is not recognized by the web server, please register it.</h2>
    <form action="/add" method="post">
      <div>Board:<textarea name="boardid">DEFAULTKEY</textarea></div>
      <div>CompanyID:<textarea name="companyid" rows="1" cols="60"></textarea></div>
      <div>passphrase<textarea name="passphrase" rows="1" cols="60"></textarea></div>
      <div><input type="submit" value="submit"></div>
    </form>
  </body>
</html>
'''

RESPONSE_PAGE_HTML = '''\
<html>
	<head>
		<title>mbed Powered Scavenger Hunt</title>
		<meta name="description" content="DEFAULTCOMPANY's password is 'DEFAULTVALUE'">
	</head>
	<body>
		<b>DEFAULTCOMPANY</b>'s password is '<i>DEFAULTVALUE</i>'

	</body>

</html>
'''

# main page checks dictionary 
#	if key exists display value
#	if key doesnt exist add it to dictionary
class MainPage(webapp2.RequestHandler):
	def get(self):
		x = self.request.get('q').lower()
		if x in register.keys():
			self.response.write(RESPONSE_PAGE_HTML.replace('DEFAULTVALUE',register[x]['passphrase']).replace('DEFAULTCOMPANY',register[x]['companyid']))
		else:
			self.response.write(ADD_PAGE_HTML.replace('DEFAULTKEY',x))


# the debug page prints out all key value pairs
class DebugPage(webapp2.RequestHandler):
	def get(self):
		pp = pprint.pformat(register,indent=6)
		self.response.write(pp)

# the add page takes a post message and adds the keys and values to the dictionary
class AddPage(webapp2.RequestHandler):
	def post(self):
		boardid   	= cgi.escape(self.request.get('boardid'))
		companyid 	= cgi.escape(self.request.get('companyid'))
		passphrase 	= cgi.escape(self.request.get('passphrase'))
		register[boardid] = {'passphrase':passphrase,'companyid':companyid}
		self.response.write("Added company:'<b>"+companyid+"</b>' with passphrase:'<i>"+passphrase+"</i>'")

application = webapp2.WSGIApplication(
	[
    	('/', MainPage),
    	('/debug',DebugPage),
    	('/add',AddPage),
], debug=True)
Revision:
16:1daa78939a3b
Parent:
14:868a1207022d
Child:
17:e2c0a1696e39
--- a/main.cpp	Fri Mar 06 09:23:50 2015 +0000
+++ b/main.cpp	Mon Mar 09 16:35:03 2015 +0000
@@ -14,244 +14,49 @@
  * limitations under the License.
  */
 
-#include <stdint.h>
-#include <stddef.h>
-#include <nrf_error.h>
 #include "mbed.h"
 #include "BLEDevice.h"
 #include "URIBeaconConfigService.h"
 #include "DFUService.h"
-#include "pstorage.h"
 #include "DeviceInformationService.h"
-
-// Struct to hold persistent data across power cycles
-struct PersistentData_t {
-    uint32_t magic;
-    URIBeaconConfigService::Params_t params;
-    uint8_t pad[4];
-} __attribute__ ((aligned (4)));
-
-static const int PERSISTENT_DATA_ALIGNED_SIZE = sizeof(PersistentData_t);
-// Seconds after power-on that config service is available.
-static const int ADVERTISING_TIMEOUT_SECONDS = 60;
-// Advertising interval for config service.
-static const int ADVERTISING_INTERVAL_MSEC = 1000;
-// Maximum size of service data in ADV packets
-static const int SERVICE_DATA_MAX = 31;
-// Magic that identifies persistent storage
-static const uint32_t MAGIC = 0x1BEAC000;
-// Values for ADV packets related to firmware levels
-static URIBeaconConfigService::PowerLevels_t  defaultAdvPowerLevels = {-20, -4, 0, 10};
-// Values for setTxPower() indexed by power mode.
-static const int8_t firmwarePowerLevels[] = {-20, -4, 0, 10};
+#include "ConfigParamsPersistence.h"
 
 BLEDevice ble;
 URIBeaconConfigService *uriBeaconConfig;
-pstorage_handle_t pstorageHandle;
-PersistentData_t  persistentData;
 
-/* LEDs for indication */
-DigitalOut  connectionStateLed(LED1);
-DigitalOut  advertisingStateLed(LED2);
-
-/* Dummy callback handler needed by Nordic's pstorage module. */
-void pstorageNotificationCallback(pstorage_handle_t *p_handle,
-                                  uint8_t            op_code,
-                                  uint32_t           result,
-                                  uint8_t *          p_data,
-                                  uint32_t           data_len) {
-    /* APP_ERROR_CHECK(result); */
-}
-
-void pstorageLoad() {
-    pstorage_init();
-    pstorage_module_param_t pstorageParams = {
-        .cb          = pstorageNotificationCallback,
-        .block_size  = PERSISTENT_DATA_ALIGNED_SIZE,
-        .block_count = 1
-    };
-    pstorage_register(&pstorageParams, &pstorageHandle);
-    if (pstorage_load(reinterpret_cast<uint8_t *>(&persistentData),
-                      &pstorageHandle, PERSISTENT_DATA_ALIGNED_SIZE, 0) != NRF_SUCCESS) {
-        // On failure zero out and let the service reset to defaults
-        memset(&persistentData, 0, sizeof(PersistentData_t));
-    }
-}
-
-
-void pstorageSave() {
-    if (persistentData.magic != MAGIC) {
-        persistentData.magic = MAGIC;
-        pstorage_store(&pstorageHandle,
-                       reinterpret_cast<uint8_t *>(&persistentData),
-                       sizeof(PersistentData_t),
-                       0 /* offset */);
-    } else {
-        pstorage_update(&pstorageHandle,
-                        reinterpret_cast<uint8_t *>(&persistentData),
-                        sizeof(PersistentData_t),
-                        0 /* offset */);
-    }
-}
-
-void startAdvertisingUriBeaconConfig() {
-    char  DEVICE_NAME[] = "mUriBeacon Config";
-
-    ble.clearAdvertisingPayload();
-
-    // Stops advertising the UriBeacon Config Service after a delay
-    ble.setAdvertisingTimeout(ADVERTISING_TIMEOUT_SECONDS);
-
-    ble.accumulateAdvertisingPayload(
-        GapAdvertisingData::BREDR_NOT_SUPPORTED |
-        GapAdvertisingData::LE_GENERAL_DISCOVERABLE);
-
-    // UUID is in different order in the ADV frame (!)
-    uint8_t reversedServiceUUID[sizeof(UUID_URI_BEACON_SERVICE)];
-    for (unsigned int i = 0; i < sizeof(UUID_URI_BEACON_SERVICE); i++) {
-        reversedServiceUUID[i] =
-            UUID_URI_BEACON_SERVICE[sizeof(UUID_URI_BEACON_SERVICE) - i - 1];
-    }
-    ble.accumulateAdvertisingPayload(
-        GapAdvertisingData::COMPLETE_LIST_128BIT_SERVICE_IDS,
-        reversedServiceUUID,
-        sizeof(reversedServiceUUID));
-
-    ble.accumulateAdvertisingPayload(GapAdvertisingData::GENERIC_TAG);
-    ble.accumulateScanResponse(
-        GapAdvertisingData::COMPLETE_LOCAL_NAME,
-        reinterpret_cast<uint8_t *>(&DEVICE_NAME),
-        sizeof(DEVICE_NAME));
-    ble.accumulateScanResponse(
-        GapAdvertisingData::TX_POWER_LEVEL,
-        reinterpret_cast<uint8_t *>(
-            &defaultAdvPowerLevels[URIBeaconConfigService::TX_POWER_MODE_LOW]),
-        sizeof(uint8_t));
-
-    ble.setTxPower(
-        firmwarePowerLevels[URIBeaconConfigService::TX_POWER_MODE_LOW]);
-
-    ble.setDeviceName(reinterpret_cast<uint8_t *>(&DEVICE_NAME));
-    ble.setAdvertisingType(GapAdvertisingParams::ADV_CONNECTABLE_UNDIRECTED);
-    ble.setAdvertisingInterval(
-        Gap::MSEC_TO_ADVERTISEMENT_DURATION_UNITS(ADVERTISING_INTERVAL_MSEC));
+void disconnectionCallback(Gap::Handle_t handle, Gap::DisconnectionReason_t reason)
+{
     ble.startAdvertising();
 }
 
-void startAdvertisingUriBeacon() {
-    uint8_t serviceData[SERVICE_DATA_MAX];
-    int serviceDataLen = 0;
-
-    advertisingStateLed = 1;
-    connectionStateLed = 1;
-
-    ble.shutdown();
-    ble.init();
-
-    // Fields from the Service
-    int beaconPeriod  = persistentData.params.beaconPeriod;
-    int txPowerMode   = persistentData.params.txPowerMode;
-    int uriDataLength = persistentData.params.uriDataLength;
-    URIBeaconConfigService::UriData_t &uriData = persistentData.params.uriData;
-    URIBeaconConfigService::PowerLevels_t &advPowerLevels = persistentData.params.advPowerLevels;
-    uint8_t flags = persistentData.params.flags;
-
-    pstorageSave();
-
-    delete uriBeaconConfig;
-    uriBeaconConfig = NULL;
-
-    ble.clearAdvertisingPayload();
-    ble.setTxPower(firmwarePowerLevels[txPowerMode]);
-
-    ble.setAdvertisingType(
-        GapAdvertisingParams::ADV_NON_CONNECTABLE_UNDIRECTED);
-
-    ble.setAdvertisingInterval(
-        Gap::MSEC_TO_ADVERTISEMENT_DURATION_UNITS(beaconPeriod));
-
-    ble.accumulateAdvertisingPayload(
-        GapAdvertisingData::BREDR_NOT_SUPPORTED |
-        GapAdvertisingData::LE_GENERAL_DISCOVERABLE);
-
-    ble.accumulateAdvertisingPayload(
-        GapAdvertisingData::COMPLETE_LIST_16BIT_SERVICE_IDS, BEACON_UUID,
-        sizeof(BEACON_UUID));
-
-    serviceData[serviceDataLen++] = BEACON_UUID[0];
-    serviceData[serviceDataLen++] = BEACON_UUID[1];
-    serviceData[serviceDataLen++] = flags;
-    serviceData[serviceDataLen++] = advPowerLevels[txPowerMode];
-    for (int j=0; j < uriDataLength; j++) {
-        serviceData[serviceDataLen++] = uriData[j];
-    }
-
-    ble.accumulateAdvertisingPayload(
-        GapAdvertisingData::SERVICE_DATA,
-        serviceData, serviceDataLen);
-
-    ble.startAdvertising();
-}
-
-// After advertising timeout, stop config and switch to UriBeacon
-void timeout(void) {
-    Gap::GapState_t state;
-    state = ble.getGapState();
-    if (!state.connected) {
-        startAdvertisingUriBeacon();
-    }
-}
-
-// When connected to config service, change the LEDs
-void connectionCallback(Gap::Handle_t handle,
-                        Gap::addr_type_t peerAddrType,
-                        const Gap::address_t peerAddr,
-                        const Gap::ConnectionParams_t *params) {
-    advertisingStateLed = 1;
-    connectionStateLed = 0;
-}
-
-// When disconnected from config service, start advertising UriBeacon
-void disconnectionCallback(Gap::Handle_t handle,
-                           Gap::DisconnectionReason_t reason) {
-    advertisingStateLed = 0;    // on
-    connectionStateLed = 1;     // off
-    startAdvertisingUriBeacon();
-}
-
 int main(void)
 {
-    advertisingStateLed = 0; // on
-    connectionStateLed  = 1; // off
-
     ble.init();
     ble.onDisconnection(disconnectionCallback);
-    ble.onConnection(connectionCallback);
-    // Advertising timeout
-    ble.onTimeout(timeout);
-
-    pstorageLoad();
-    bool resetToDefaults = persistentData.magic != MAGIC;
 
-    URIBeaconConfigService::UriData_t defaultUriData;
-    size_t                            defaultUriDataLength;
-    URIBeaconConfigService::encodeURI("http://uribeacon.org", defaultUriData, defaultUriDataLength);
+    /*
+     * Load parameters from (platform specific) persistent storage. Parameters
+     * can be set to non-default values while the URIBeacon is in configuration
+     * mode (within the first 60 seconds of power-up). Thereafter, parameters
+     * get copied out to persistent storage before switching to normal URIBeacon
+     * operation.
+     */
+    URIBeaconConfigService::Params_t params;
+    loadURIBeaconConfigParams(&params);
 
-    uriBeaconConfig = new URIBeaconConfigService(ble, persistentData.params, resetToDefaults,
-                                                 defaultUriData, defaultUriDataLength, defaultAdvPowerLevels);
+    /* Initialize a URIBeaconConfig service providing config params, default URI, and power levels. */
+    static URIBeaconConfigService::PowerLevels_t defaultAdvPowerLevels = {-20, -4, 0, 10}; // Values for ADV packets related to firmware levels
+    uriBeaconConfig = new URIBeaconConfigService(ble, params, "http://uribeacon.org", defaultAdvPowerLevels);
     if (!uriBeaconConfig->configuredSuccessfully()) {
         error("failed to accommodate URI");
     }
 
     // Setup auxiliary services to allow over-the-air firmware updates, etc
     DFUService dfu(ble);
-    DeviceInformationService deviceInfo(
-        ble, "ARM", "UriBeacon", "SN1", "hw-rev1", "fw-rev1", "soft-rev1");
+    DeviceInformationService deviceInfo(ble, "ARM", "UriBeacon", "SN1", "hw-rev1", "fw-rev1", "soft-rev1");
 
-    /* Start out by advertising the configService for a limited time after
-     * startup; and switch to the normal non-connectible beacon functionality
-     * afterwards. */
-    startAdvertisingUriBeaconConfig();
+    ble.startAdvertising(); /* Set the whole thing in motion. After this call a GAP central can scan the URIBeaconConfig
+                             * service, which then automatically switches to the URIBeacon after a timeout. */
 
     while (true) {
         ble.waitForEvent();