Bluetooth scanning and advertising with LTE Beacon

If there’s one thing all Estimote Beacons have in common, it’s their Bluetooth capabilities.

The LTE Beacon is no different. It can advertise any Bluetooth data, and it can also detect advertisements from other Bluetooth devices.

What’s ahead (aka Table of Contents)


Before we jump into the scanning examples, we’ll need something to scan for. If you already have any other Estimote Beacons, or maybe some other iBeacon-compatible beacons, you should be all set!

If you don’t, you can make one of your LTE Beacon advertise, and then we’ll have the other one scan and detect it.

In Estimote Cloud, create a new IoT App, call it “Advertising”, and use this micro-app code:

var packet =;
var handle = ble.advertise(packet);

This will make the LTE Beacon broadcast an Estimote Location packet with the LTE Beacon’s identifier. We also set this to happen every 500 ms, and the transmit power to -4 dBm.

Tip: -4 is relatively strong, so the beacon should be detectable at a longer distance. Other options are, from the weakest to the strongest: -40, -20, -16, -12, -8, -4, 0, 4. You can experiment with them if you want, to find something that suits you!


Let’s look at two examples of how you can use the Bluetooth scanning in your LTE Beacon.

We’ll assume you either have some Estimote Proximity or Location beacons nearby with Estimote Monitoring enabled, or that you programmed one of your LTE Beacons to broadcast the Estimote Location packet as shown above.

Asset tracking example

Our idea here is, every hour, let’s start Bluetooth scanning for a short moment, detect all the other beacons nearby, and queue an “assets-update” event with the identifiers of the beacons we found.

var SCAN_INTERVAL = 60 * 60 * 1000; // 60 minutes in milliseconds
var SCAN_DURATION = 10 * 1000; // 10 seconds in milliseconds

var detectedBeacons = {};

timers.repeat(SCAN_INTERVAL, () => {
        // 1st argument -- callback to process each scan result
        (scanResult) => {
            var packet = ble.parse(scanResult);
            if (packet && packet.type === 'est_loc') {
                detectedBeacons[] = true;
        // 2nd argument -- how long to scan for
    ).then(() => {
        var assets = Object.keys(detectedBeacons);
        cloud.enqueue('assets-update', {assets: assets});
        detectedBeacons = {};

In our scan-processing callback (“1st argument”), we try to parse the raw data, and if the result is a valid Estimote Location packet, we add the identifier from that packet to detectedBeacons. We use a JavaScript object instead of an array for detectedBeacons, as a simple way to handle duplicate detections.

Next, let’s have a closer look at this part:

  // ...
).then(() => {
    /* 1 */ var assets = Object.keys(detectedBeacons);
    /* 2 */ cloud.enqueue('assets-update', {assets: assets});
    /* 3 */ detectedBeacons = {};

This means pretty much exactly how it reads: start scanning, and then (= when the scanning stops), do some things. (More formally, startScan returns a promise which resolves when the scan stops.) This gives us the perfect opportunity to “sum up” our findings, and enqueue the “assets-update” event:

  • /* 1 */, we use Object.keys(detectedBeacons) to transform our JavaScript object to a simple assets array of identifiers. In other words, it’ll go from something like this:

    {"1a15b246a7190f625c4fdbf29b030f06": true,
     "f04c16c4905ae0790bcd4302da86762a": true}

    to something like this:

    ["1a15b246a7190f625c4fdbf29b030f06", "f04c16c4905ae0790bcd4302da86762a"]
  • /* 2 */, we enqueue this array to be sent to Estimote Cloud in an “assets-update” event.

  • /* 3 */, we reset detectedBeacons back to an empty object, so that next time startScan runs, old data won’t mix with the new.

Finally, since the whole startScan code is inside timers.repeat:

timers.repeat(SCAN_INTERVAL, () => {
    // ...

… it’ll run periodically—in our case, every hour. You can adjust the SCAN_INTERVAL in the code above if you want, but remember that the more frequently you scan, the shorter bettery life of your LTE Beacon is going to be. You may want to consider just keeping it plugged to power.

One last thing to consider: when/how often to sync the “assets-update” event to Estimote Cloud. You could opt to do this periodicially, for example, once a day:

// add all of this, for example, at the top of the file
var oneDay = 24 * 60 * 60; // sync period is in seconds

Or, you could force a sync on every event:

cloud.enqueue('assets-update', {assets: assets});; // <== add this below the 'enqueue'

Just remember about the battery life implications, and that the base-tier Estimote Cloud subscription comes with 1000 syncs per each LTE Beacon per year.

Simple indoor positioning example

Another idea is: just like the LTE Beacon can determine its position outdoors via GPS and satellites, it can also determine its position indoors via Bluetooth and other beacons deployed throughout the venue. This is very similar to smartphone apps and Estimote’s Proximity and Indoor Location SDKs!

There’s more than one way to approach this, but let’s try something simple: upon a button press, the LTE Beacon will scan for other Estimote Beacons nearby. When it finds one, it’ll queue an “indoor-position-update” event, and immediately sync it to Estimote Cloud.

var SCAN_DURATION = 10 * 1000; // 10 seconds in milliseconds

var foundBeacon = null; => {
    var bleScan = ble.startScan(
        /* 1 */ (scanResult) => {
            var packet = ble.parse(scanResult);
            if (packet && packet.type === 'est_loc') {
                foundBeacon =;
        /* 2 */ SCAN_DURATION
    ).then(() => { /* 3 */
        cloud.enqueue('indoor-position-update', {beacon: foundBeacon});;
        foundBeacon = null;

As you can see, the code is actually pretty similar to the “asset tracking” example. It’s mostly the situation that’s different: instead of a static LTE Beacon scanning for roaming beacons–assets, here we assume a roaming LTE Beacon scanning for static beacons–anchor-points.

In any case, let’s once again look at the 3 arguments we pass to the startScan function:

  1. First, in the scan-handling callback, we say that when the LTE Beacon finds a valid Estimote Location packet, it should store the identifier from that beacon under foundBeacon, and stop the scan.

  2. Second, we say the the scan should run for SCAN_DURATION (here: 10 seconds). Note that in this example, the scan can also stop sooner, per our code in the scan-handling callback from #1. So the 10 seconds here mean that if we can’t find anything in that time, we’ll give up and foundBeacon will remain null.

  3. Finally, some code to run when the scanning stops. Note that it doesn’t matter if the scan stopped because it’s run its course (= 10 seconds), or because we stopped it ourselves in #1, with bleScan.stop().

    Here we queue an “indoor-position-update” event, and attach the foundBeacon to it—which will be either an identifier of a beacon we detected, or null if we haven’t found anything within the 10 seconds. We also immediately force a sync to Estimote Cloud. And, after all that, we reset foundBeacon back to null.

You could try to further improve this solution by adding some basic RSSI filtering to pick a beacon with the strongest signal. You can access the RSSI via scanResult.rssi. Consider this excercise a completely optional homework ;-)

Addendum: List of packets supported by the built-in parser

The ble.parse function we mentioned & used above supports many Estimote & other popular BLE advertising packets, out of the box. Here’s a list.

Estimote family

Estimote Location

Used by Estimote Proximity and Location Beacons to power Estimote Monitoring and Indoor Location.

{type: "est_loc",
 id: "123f2d5e3ad9d9d4fcd4efb254c56119", // beacon ID
 power: -62 // measured power = RSSI expected at 1 m from beacon
            // this is commonly used in distance-estimation math

Estimote Nearable

Used by Estimote Stickers.

See also:

{type: "estimote_nearable",
 id: "fcd4efb254c56119", // Nearable ID
 temperature: 22.25, // [°C]
 voltage: 2.89, [V]
 voltageType: "stress" // or "idle"
 moving: true,
 acc: {x: 1.12, y: 0.21, z: -0.01}, // acceleration [g]
 currMotionDur: 3, // how long has the beacon been moving/staying still [s]
 prevMotionDur: 65, // before it started/stopped moving,
                    // how long was the beacon in its previous state [s]
 txPower: -78, // measured power at 1 m [dBm]
 channel: 37 // physical BLE channel on which this packet was advertised

Estimote Telemetry

This packet comes in two variants, advertised in turns.

See also:

Note that the actual availability of sensors depends on the exact model of the Estimote Beacon.

  • “subframe A”

    {type: "est_tlm",
     shortId: "277b15125db94314", // first half of the beacon ID
     subframe: "A",
     motion: true, // moving, or not moving, that is the question
     acc: {x: 1.12, y: 0.21, z: -0.01}, // acceleration [g]
     currMotionDur: 3, // how long has the beacon been moving/staying still [s]
     prevMotionDur: 65, // before it started/stopped moving,
                        // how long was the beacon in its previous state [s]
     pressure: 1234, // atmospheric pressure [Pa]
     g0: false, // state of the GPIO pin 0
     g1: false // state of the GPIO pin 1
  • “subframe B”

    {type: "est_tlm",
     shortId: "277b15125db94314", // first half of the beacon ID
     subframe: "B",
     mag: {"x": 0.0, "y": 0.0, "z": 0.0}, // magnetometer
     light: 0, // ambient light [lux]
     temp: 24.8125, // [°C]
     batteryVoltage: 2.948, // [V]
     batteryLevel: 0.38, // estimated battery level [%] (1.0 = 100%)
     uptime: 34992000 // time since last beacon restart [s]

Estimote Connectivity

Used to connect to Estimote Proximity and Location Beacons—for example, for configuration or firmware updates.

{type: "est_conn",
 id: "123f2d5e3ad9d9d4fcd4efb254c56119", // beacon ID
 fw_ver: "4.12.00", // firmware version
 boot_ver: "4.00.09" // bootloader version

Estimote Mirror

{type: "est_mirror",
 id: "c87250041e0962dcd103003cb1891a0c",
 power: -41, // measured power at 1 m [dBm]
 access: true


Probably The Most Famous Beacon Packet, by Apple. See for more.

{type: "ibeacon",
 uuid: "b9407f30f5f8466eaff925556b57fe6d",
 major: 123,
 minor: 123,
 power: -55 // measured power at 1 m [dBm]


The Open and Interoperable Beacon Specification. See for more.

{type: "altbeacon",
 manufacturer: 349,
 beaconId: "b9407f30f5f8466eaff925556b57fe6d007b007b",
 power: -55, // measured power at 1 m [dBm]
 rfu: 0

Eddystone family

An open beacon protocol by Google. See for more.

Comes in a few variants:


{type: "eddystone_url",
 url: "",
 power: -22 // measured power at 0 m [dBm]


{type: "eddystone_uid",
 namespace: "eddd1ebeac04e5defa99",
 instance: "332c183ed231",
 power: -18, // measured power at 0 m [dBm]


{type: "eddystone_tlm",
 vbatt: 3.01, // [V]
 temp: 22.34, // [°C]
 pduCount: 6546,
 uptime: 47658.5 // [s]


{type: "eddystone_eid",
 eid: "5db9437b15121427",
 power: -19 // measured power at 0 m [dBm]

Addendum: Parsing custom BLE packets (= scan result reference)

So far, we’ve been passing the scanResult to the ble.parse function, to let the built-in parser do its job.

ble.startScan((scanResult) => {
    var packet = ble.parse(scanResult);
    if (packet && packet.type === 'est_loc') {

// ...

However, you can also access the scanResult directly—for example, to parse a custom BLE packet. Here’s a list of properties available on the scanResult objects.

Always available:

  • rssi - [integer number]
  • channel - physical BLE channel on which the packet was received [integer number]
  • scanResponse - [boolean], true if the packet is a scan response
  • connectable - [boolean], true if the packet is connectable
  • addr - address of the broadcasting device [string]
    • hexadecimal, lowercase, no leading “0x”

Other properties, and the AD Types they correspond to:

  • name - Complete/Shortened Local Name [string]
  • serviceData["XXXX"] - Service Data for the 16-bit “XXXX” Service UUID [ArrayBuffer]
    • replace XXXX with the 16-bit Service UUID you’re looking for
    • format of the string: hexadecimal, uppercase, no leading “0x”
    • for example, for Estimote’s Service UUID, that’d be FE9A
    • non-16-bit Service UUIDs are not currently supported
  • manufData - Manufacturer Specific Data [ArrayBuffer]
    • the Bluetooth spec mandates that the first two bytes shall be the Company Identifier, in little-endian (for example, Estimote’s 0x015D is broadcast as: first byte = 0x5D, second byte = 0x01)
  • flags - Flags [integer Number]
  • txPower - Tx Power Level [integer Number]
  • serviceUUIDs - Incomplete/Complete List of 16/32/128-bit Service UUIDs [Array of ArrayBuffers]

If AD Types not described above are present in the packet, they will be exposed like this:

// 0x19 => "Appearance" AD Type
scanResult["19"] => ArrayBuffer
// the format of the key-string is hexadecimal, lowercase, no leading "0x"

Here’s an example of a custom parser that’s looking for a packet with local name set to “My BLE Clock”, appearance set to a “generic clock”, and the first two bytes of the Estimote service data are 0xBE and 0xEF. The use of the 16‑bit Service UUID assigned to Estimote is just for the sake of the example … or is Estimote moving into the smart clocks territory? :wink: :wink:

ble.startScan((scanResult) => {
    if ( !== "My BLE Clock") { return; }

    var appearanceBuffer = scanResult["19"];
    if (!appearanceBuffer) { return; }

    var appearanceBytes = new UInt8Array(appearanceBuffer);
    if (appearanceBytes[0] !== 256) { // 256 = generic clock

    var dataBuffer = scanResult.serviceData && scanResult.serviceData["FE9A"];
    if (!dataBuffer) { return; }

    var dataBytes = new UInt8Array(dataBuffer);
    var isAlarm = dataBytes[0] === 0xBE && dataBytes[1] === 0xEF;
    if (isAlarm) {
      cloud.enqueue('alarm', {clockAddr: scanResult.addr});

// ...