Part 3: Ranging beacons

In part 2, we’ve learned how to monitor beacons in our app in order to be notified whenever the device enters and exits range of these beacons.

Monitoring is very powerful in certain ways, but it’s more of a coarse-grained mechanism.

Fortunately, beacon monitoring is complemented by another feature of the Estimote SDK: ranging beacons. We’ll use this one in our airport app with the intent to guide users to nearby snack bars. (Don’t know about you, but we’re always hungry before our flights, and Yelp or Foursquare have a tendency to recommend food spots located just before the security checkpoint we’ve just passed.)

You can download the full source code of the tutorial and follow along.

What’s ahead (aka Table of Contents)

What is ranging?

While monitoring creates a virtual fence to detect when you’re moving in and out, ranging actively scans for any nearby beacons and delivers results to you every second.

Let’s say we defined a “terminal Z” region. With monitoring, our app will be notified whenever the user enters and exits the terminal. But if we start ranging for the exact same region, we’ll instead get a full list of matching beacons currently in range—complete with their UUID, major, and minor values.

There’s also another, more distinct feature of ranging—one that deserves its own paragraph.

Proximity estimation

Each beacon broadcasts its Bluetooth signal with a certain strength—a strength which diminishes as the signal travels through the air. This enables the receiver device to make a rough estimation of how far the beacon is. Strong signal = it’s close. Weak signal = it’s further away.

Info: It’s actually an inverse-square relationship, i.e., if a distance to the beacon increases two times, the signal strength decreases four times. This makes the accuracy of the proximity estimations decrease drastically as the distance increases. (Signal strength at 20 meters will be roughly 100x weaker than at 2 meters!)

Ranging utilizes the differences in received signal strength to:

  • (a) determine which beacons are likely closer, and which are probably further away from the device,
  • (b) categorize the beacons into four proximity zones:
    • immediate (strong signal; usually up to a few centimeters)
    • near (medium signal; usually up to a few meters)
    • far (weak signal; more than a few meters)
    • unknown (“hard to say”, usually when the signal is very, very weak)

Important: While received signal strength, proximity zone and accuracy values can theoretically be used to derive a distance estimation, in practice this is far from trivial and requires complex mathematical models to account for fluctuations in the signal strength.

Long story short: do not expect distance estimations from beacons.

The trade-offs

Ranging provides more granular and comprehensive beacon data, but this comes at the expense of draining the battery faster than monitoring. This means that it’s usually not a good idea to run ranging for extended periods of time, e.g., hours. It certainly wouldn’t be viable to run it at all times.

Start ranging

Starting ranging is very similar to starting monitoring—we also need to provide a beacon region that will define which beacons to scan for. Let’s say we’re interested in all the beacons installed at the airport. For that, we can use one of the broader variants of defining a beacon region—by UUID only.

Ideally, you’d assign a dedicated UUID to all of the airport beacons—but for the purposes of this tutorial, let’s just use the default Estimote UUID: B9407F30-F5F8-466E-AFF9-25556B57FE6D. (If you’ve changed the UUID since you got your beacons, you’ll need to adjust the following code appropriately.)

Since our “food places nearby” feature is tied to a particular screen, we’ll cut a corner and put a second beacon manager in the Activity corresponding to the screen. This’ll allow us to manipulate the view directly in the ranging listener. (A more idiomatic way would be to have the beacon manager we already have in the application class to manipulate a data model, and the Activity observing the changes to the data model instead. We just want to keep thing simple in this tutorial.)

Let’s go to the MainActivity implementation file and set up a second beacon manager. Also, this time, we’ll create a dedicated property to hold the beacon region, since we’ll be using it in two places: to start, and to stop ranging. This goes inside the MainActivity class:

private BeaconManager beaconManager;
private BeaconRegion region;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    beaconManager = new BeaconManager(this);
    region = new BeaconRegion("ranged region",
            UUID.fromString("B9407F30-F5F8-466E-AFF9-25556B57FE6D"), null, null);
}

Now, the code to start and stop ranging as the activity appears and disappears on screen. This too goes inside the MainActivity class:

@Override
protected void onResume() {
    super.onResume();

    SystemRequirementsChecker.checkWithDefaultDialogs(this);

    beaconManager.connect(new BeaconManager.ServiceReadyCallback() {
        @Override
        public void onServiceReady() {
            beaconManager.startRanging(region);
        }
    });
}

@Override
protected void onPause() {
    beaconManager.stopRanging(region);

    super.onPause();
}

Tying beacon data and app data together

Our monitoring use case was quite trivial—just show a notification. With ranging, our goal is to show nearby options to a grab a snack, which involves tying two pieces of data together: identifiers of nearby beacons, and a list of food options. (In general, since beacons only broadcast their identifiers, this is something you’ll end up doing quite often when developing beacon apps.)

First, we need a data structure to hold the food options and beacons.

Second, we need to code up a simple algorithm:

  1. Take the closest beacon.
  2. Look up all the food places closest to that beacon.

Again, to keep this tutorial short and about beacons, we’re going to cut more corners:

  • We’ll use a hard-coded list of beacons and food places. Ideally, we’d fetch this from a web API of some sorts.
  • We’ll stop at computing the sorted list of food places to present to the user, leaving the implementation of the UI to the reader. Hint, hint, list view? (-:

Let’s start by adding the data. Once again, this goes inside the MainActivity class body:

private static final Map<String, List<String>> PLACES_BY_BEACONS;

// TODO: replace "<major>:<minor>" strings to match your own beacons.
static {
    Map<String, List<String>> placesByBeacons = new HashMap<>();
    placesByBeacons.put("22504:48827", new ArrayList<String>() {{
        add("Heavenly Sandwiches");
        // read as: "Heavenly Sandwiches" is closest
        // to the beacon with major 22504 and minor 48827
        add("Green & Green Salads");
        // "Green & Green Salads" is the next closest
        add("Mini Panini");
        // "Mini Panini" is the furthest away
    }});
    placesByBeacons.put("648:12", new ArrayList<String>() {{
        add("Mini Panini");
        add("Green & Green Salads");
        add("Heavenly Sandwiches");
    }});
    PLACES_BY_BEACONS = Collections.unmodifiableMap(placesByBeacons);
}

We went for list nested inside a map. The map maps (ha!) beacon’s major and minor (clumped together in a <major>:<minor> string) to the list of names of food places, pre-sorted by starting with the nearest ones. All in all, nothing too sophisticated or even elegant, but for this simple example it’ll do.

Remember to replace the majors and minors with those of your beacons!

Info: We intentionally omitted the UUID of beacons, since we specified it as part of the beacon region definition we’re passing to the startRanging method.

We’ll now implement a method that takes a Beacon object representing the closest beacon, and return a list of all the places sorted by their distance to the beacon:

private List<String> placesNearBeacon(Beacon beacon) {
    String beaconKey = String.format("%d:%d", beacon.getMajor(), beacon.getMinor());
    if (PLACES_BY_BEACONS.containsKey(beaconKey)) {
        return PLACES_BY_BEACONS.get(beaconKey);
    }
    return Collections.emptyList();
}

Ranging listener

Recall that the list of ranged beacons is already sorted from the (likely) nearest to the (likely) furthest ones.

Info: Yes, we’re very persistent with the likely keyword. All the proximity estimations for beacons are based on the signal strength, which is naturally susceptible to fluctuations. It will occasionally happen that a beacon further away is considered closer than the actual nearest beacon, or even show up in the ranging results before the nearest one does.

Temporarily anomalies don’t hurt our airport app—the order of places will simply be off for a moment. However, if you’re building a museum app which automatically starts a new audio guide every time the closest beacon changes to a new one, it’s better to take ranging results with a pinch of salt.

Having the list pre-sorted by the Estimote SDK, and with all the prep work we’ve performed, the ranging listener turns out to be quite simple:

// find this line inside the `onCreate` method:
beaconManager = new BeaconManager(this);
// add this below:
beaconManager.setRangingListener(new BeaconManager.BeaconRangingListener() {
    @Override
    public void onBeaconsDiscovered(BeaconRegion region, List<Beacon> list) {
        if (!list.isEmpty()) {
            Beacon nearestBeacon = list.get(0);
            List<String> places = placesNearBeacon(nearestBeacon);
            // TODO: update the UI here
            Log.d("Airport", "Nearest places: " + places);
        }
    }
});

That’s it! (Apart from adding the UI of course.) Run the app on your device, spread the beacons around and move the phone from one to another. The order of places printed in the console should keep changing depending on which beacon is the closest.

Key takeaways

  • Beacon ranging provides fine-grained data about beacons detected nearby, as opposed to monitoring’s coarse-grained “inside region” and “outside region.” The data includes exact UUID, major, and minor values of ranged beacons, as well as proximity estimations.

  • Proximity estimations are based on received signal strength, and are good to roughly determine if the device is close to or far away from a beacon. Beacons are not meant to provide distance estimations!

  • Use startRanging and stopRanging to control ranging. Ranging results are delivered every second to the listener. Ranging results is an array of Beacon objects, sorted from the beacons likely closest to the device to those likely further away.

Download the full source code