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 advHandle = ble.advertise(packet);
// later, if you want to stop advertising:

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: Other transmit power options are, from the weakest to the strongest: -40, -20, -16, -12, -8, -4, 0, 4, 8, 20. 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 = '1 hour';
var SCAN_DURATION = '10 secs';
// supported suffixes are:
// ms, s/sec/secs, m/min/mins, h/hour/hours, d/day/days, w/week/weeks

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 this, for example, at the top of the file
sync.setSyncPeriod('1 day');

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 secs';

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 ;-)

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

or in newer versions:

 type: "est_conn",
 id: "123f2d5e3ad9d9d4fcd4efb254c56119", // beacon ID
 flags: 1 // device status flags

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]

Parsing custom BLE packets

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:

  • type - [integer number] packet type
    • 0 - Indirect advertising packet
    • 1 - Scan response
    • 2 - Scan request (not supported yet)
  • rssi - [integer number]
  • channel - physical BLE channel on which the packet was received [integer number - 37, 38, 39]
  • 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”
  • addrType - [integer number] address type
    • 0x00 - public
    • 0x01 - random static
    • 0x02 - random private resolvable
    • 0x03 - random private non-resolvable
    • 0x7F - anonymous

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 appearanceShorts = new Uint16Array(appearanceBuffer);
    if (appearanceShorts[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});

// ...

Advertising custom BLE packets

At the very top of this page, we’ve shown how to quickly advertise common BLE packets:

var packet =;
// see* in the micro-app API reference for more packet types
var advHandle = ble.advertise(packet);

However, you can also advertise your own, custom BLE data as well:

var data = {
  name: 'My Beacon',
  serviceData: {
    'FE9A': 'BEEF'
var advHandle = ble.advertise(data);

Properties you can have on the object that you pass to advertise are:

  • name - Shortened Local Name [string]
  • serviceData - key-value object with Service Data
    • keys are hexadecimal strings with a 16-bit Service UUID (for example, “FE9A”)
    • values are *data with the Service Data
    • non-16-bit Service UUIDs are not currently supported
  • manufData - Manufacturer Specific Data [*data]
    • 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] - defaults to 0x04
  • addr - address to use in the advertising packet [*data]
    • if not set, a random address will be generated each time advertise starts

*data can be provided as:

  • a hexadecimal string: 'BEEF'
  • an array of bytes-as-numbers: [0xBE, 0xEF]
  • an ArrayBuffer: Uint8Array.from([0xBE, 0xEF]).buffer

Max size of the advertising packet (after excluding the mandatory header, length, address, and flags) is currently 28 bytes. For example:

  • if you’re only advertising a name, that means max 26 characters
    (28 - 2 bytes for metadata)
  • if you’re only advertising Manufacturer Specific Data, that means max 24 bytes of data
    (28 - 2 bytes for metadata - 2 bytes for Company Identifier)

Hand-crafting custom packets from raw bytes

As an alternative to the object-notation for the advertising data, you can also pass an ArrayBuffer with raw bytes to use in the advertising packet. You should use the “Advertising physical channel PDU” described in Vol 6, part B, section 2.3 of the Bluetooth Core Specification, and specifically the “ADV_NONCONN_IND” PDU.

The “length” in the header will be populated automatically, but you should leave two bytes for it, not one. “On air”, it will be one byte like mandated by the spec, but “in memory” our firmware uses two bytes.


var rawData = Uint8Array.from([
  0x42, // header: ADV_NONCONN_IND, random Tx address
  0, 0, // two-byte length (LSB), will be set automatically
  0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, // random-static address (LSB)
  2 /* 2 bytes follow */, 0x01 /* flags */,
    0x06 /* LE General Discoverable, BR/EDR not supported */,
  10 /* 10 bytes follow */, 0x08 /* short local name */,
    chr('M'), chr('y'), chr(' '),
    chr('B'), chr('e'), chr('a'), chr('c'), chr('o'), chr('n')
var advHandler = ble.advertise(rawData);

// helper function to convert one-char string to an ASCII code
function chr(a) { return a.charCodeAt(0); }

Dynamic advertising

If you want the advertised data to change over time, you can use a variant of advertise which accepts a callback to a builder function. Whenever it’s time to advertise, that function will be called, and it should return the data to advertise. The data can be in the object-notation, or a raw ArrayBuffer, as described above.

var i = 0;
var advHandler = ble.advertise(() => {
  return {
    name: `My Counter: ${i++}`