5
5
// The protocol specification is available at:
6
6
// https://github.com/google/eddystone
7
7
8
+ ; ( function ( ) {
9
+
8
10
// prerequisites
9
11
evothings . loadScripts ( [
10
12
'libs/evothings/easyble/easyble.js' ,
11
13
] )
12
14
15
+ /**
16
+ * @namespace
17
+ * @description <p>Library for Eddystone beacons.</p>
18
+ * <p>It is safe practise to call function {@link evothings.scriptsLoaded}
19
+ * to ensure dependent libraries are loaded before calling functions
20
+ * in this library.</p>
21
+ */
13
22
evothings . eddystone = { } ;
14
23
15
- ; ( function ( ) {
16
24
// constants
17
25
var BLUETOOTH_BASE_UUID = '-0000-1000-8000-00805f9b34fb' ;
18
26
19
27
// false when scanning is off. true when on.
20
28
var isScanning = false ;
21
29
22
- /** Starts scanning for Eddystone devices.
23
- * <p>Found devices and errors will be reported to the supplied callbacks.</p>
24
- * <p>Will keep scanning indefinitely until you call stopScan().</p>
25
- * To conserve energy, call stopScan() as soon as you've found the device you're looking for.
26
- * <p>Calling this function while scanning is in progress will fail.</p>
27
- *
28
- * @param {scanCallback } win
29
- * @param {failCallback } fail
30
- */
31
- evothings . eddystone . startScan = function ( win , fail ) {
30
+ /**
31
+ * @description Starts scanning for Eddystone devices.
32
+ * <p>Found devices and errors will be reported to the supplied callbacks.</p>
33
+ * <p>Will keep scanning indefinitely until you call stopScan().</p>
34
+ * <p>To conserve energy, call stopScan() as soon as you've found the device
35
+ * you're looking for.</p>
36
+ * <p>Calling startScan() while scanning is in progress will produce an error.</p>
37
+ *
38
+ * @param {evothings.eddystone.scanCallback } - Success function called
39
+ * when a beacon is found.
40
+ * @param {evothings.eddystone.failCallback } - Error callback: fail(error).
41
+ *
42
+ * @public
43
+ *
44
+ * @example
45
+ * evothings.eddystone.startScan(
46
+ * function(beacon)
47
+ * {
48
+ * console.log('Found beacon: ' + beacon.url);
49
+ * },
50
+ * function(error)
51
+ * {
52
+ * console.log('Scan error: ' + error);
53
+ * });
54
+ */
55
+ evothings . eddystone . startScan = function ( scanCallback , failCallback )
56
+ {
57
+ // Internal callback variable names.
58
+ var win = scanCallback ;
59
+ var fail = failCallback ;
60
+
32
61
// If scanning is already in progress, fail.
33
- if ( isScanning ) {
62
+ if ( isScanning )
63
+ {
34
64
fail ( "Scanning already in progress!" ) ;
35
65
return ;
36
66
}
@@ -40,94 +70,185 @@ evothings.eddystone.startScan = function(win, fail) {
40
70
// The device object given in this callback is reused by easyble.
41
71
// Therefore we can store data in it and expect to have the data still be there
42
72
// on the next callback with the same device.
43
- evothings . easyble . startScan ( function ( device ) {
44
- // A device might be an Eddystone if it has advertisementData...
45
- var ad = device . advertisementData ;
46
- if ( ! ad ) return ;
47
- // With serviceData...
48
- var sd = ad . kCBAdvDataServiceData ;
49
- if ( ! sd ) return ;
50
- // And the 0xFEAA service.
51
- var base64data = sd [ '0000feaa' + BLUETOOTH_BASE_UUID ] ;
52
- if ( ! base64data ) return ;
53
- var byteArray = evothings . util . base64DecToArr ( base64data ) ;
54
-
55
- // If the data matches one of the Eddystone frame formats,
56
- // we can forward it to the user.
57
- if ( parseFrameUID ( device , byteArray , win , fail ) ) return ;
58
- if ( parseFrameURL ( device , byteArray , win , fail ) ) return ;
59
- if ( parseFrameTLM ( device , byteArray , win , fail ) ) return ;
60
-
61
- } , function ( error ) {
62
- fail ( error ) ;
63
- } ) ;
73
+ evothings . easyble . startScan (
74
+ function ( device )
75
+ {
76
+ // A device might be an Eddystone if it has advertisementData...
77
+ var ad = device . advertisementData ;
78
+ if ( ! ad ) return ;
79
+ // With serviceData...
80
+ var sd = ad . kCBAdvDataServiceData ;
81
+ if ( ! sd ) return ;
82
+ // And the 0xFEAA service.
83
+ var base64data = sd [ '0000feaa' + BLUETOOTH_BASE_UUID ] ;
84
+ if ( ! base64data ) return ;
85
+ var byteArray = evothings . util . base64DecToArr ( base64data ) ;
86
+
87
+ // If the data matches one of the Eddystone frame formats,
88
+ // we can forward it to the user.
89
+ if ( parseFrameUID ( device , byteArray , win , fail ) ) return ;
90
+ if ( parseFrameURL ( device , byteArray , win , fail ) ) return ;
91
+ if ( parseFrameTLM ( device , byteArray , win , fail ) ) return ;
92
+ } ,
93
+ function ( error )
94
+ {
95
+ fail ( error ) ;
96
+ } ) ;
64
97
}
65
98
66
- /** This function is a parameter to startScan() and is called when a new device is discovered.
67
- * @callback scanCallback
68
- * @param {EddystoneDevice } device
69
- */
99
+ /**
100
+ * @description This function is a parameter to startScan() and
101
+ * is called when a beacons is discovered/updated.
102
+ * @callback evothings.eddystone.scanCallback
103
+ * @param {evothings.eddystone.EddystoneDevice } beacon - Beacon
104
+ * found during scanning.
105
+ */
70
106
71
- /** Object representing a BLE device. Inherits from evothings.easyble.EasyBLEDevice.
72
- * All uninherited properties are optional; they may be missing.
73
- * @typedef {Object } EddystoneDevice
74
- * @property {string } url - An Internet URL.
75
- * @property {number } txPower - A signed integer, the signal strength in decibels, factory-measured at a range of 0 meters.
76
- * @property {Uint8Array } nid - 10-byte namespace ID.
77
- * @property {Uint8Array } bid - 6-byte beacon ID.
78
- * @property {number } voltage - Device's battery voltage, in millivolts, or 0 (zero) if device is not battery-powered.
79
- * @property {number } temperature - Device's ambient temperature in 256:ths of degrees Celcius, or 0x8000 if device has no thermometer.
80
- * @property {number } adv_cnt - Count of advertisement frames sent since device's startup.
81
- * @property {number } dsec_cnt - Time since device's startup, in deci-seconds (10 units equals 1 second).
82
- */
107
+ /**
108
+ * @description This function is called when an operation fails.
109
+ * @callback evothings.eddystone.failCallback
110
+ * @param {string } errorString - A human-readable string that
111
+ * describes the error that occurred.
112
+ */
83
113
84
- /** Stop scanning for Eddystone devices.
114
+ /**
115
+ * @description Object representing a BLE device. Inherits from
116
+ * {@link evothings.easyble.EasyBLEDevice}.
117
+ * Which properties are available depends on which packets types broadcasted
118
+ * by the beacon. Properties may be undefined. Typically properties are populated
119
+ * as scanning processes.
120
+ * @typedef {Object } evothings.eddystone.EddystoneDevice
121
+ * @property {string } url - An Internet URL.
122
+ * @property {number } txPower - A signed integer, the signal strength in decibels,
123
+ * factory-measured at a range of 0 meters.
124
+ * @property {Uint8Array } nid - 10-byte namespace ID.
125
+ * @property {Uint8Array } bid - 6-byte beacon ID.
126
+ * @property {number } voltage - Device's battery voltage, in millivolts,
127
+ * or 0 (zero) if device is not battery-powered.
128
+ * @property {number } temperature - Device's ambient temperature in 256:ths of
129
+ * degrees Celcius, or 0x8000 if device has no thermometer.
130
+ * @property {number } adv_cnt - Count of advertisement frames sent since device's startup.
131
+ * @property {number } dsec_cnt - Time since device's startup, in deci-seconds
132
+ * (10 units equals 1 second).
85
133
*/
86
- evothings . eddystone . stopScan = function ( ) {
134
+
135
+ /**
136
+ * @description Stop scanning for Eddystone devices.
137
+ * @public
138
+ * @example
139
+ * evothings.eddystone.stopScan();
140
+ */
141
+ evothings . eddystone . stopScan = function ( )
142
+ {
87
143
evothings . easyble . stopScan ( ) ;
88
144
isScanning = false ;
89
145
}
90
146
147
+ /**
148
+ * @description Calculate the accuracy (distance in meters) of the beacon.
149
+ * <p>The beacon distance calculation uses txPower at 1 meters, but the
150
+ * Eddystone protocol reports the value at 0 meters. 41dBm is the signal
151
+ * loss that occurs over 1 meter, this value is subtracted by default
152
+ * from the reported txPower. You can tune the calculation by adding
153
+ * or subtracting to param txPower.<p>
154
+ * <p>Note that the returned distance value is not accurate, and that
155
+ * it fluctuates over time. Sampling/filtering over time is recommended
156
+ * to obtain a stable value.<p>
157
+ * @public
158
+ * @param txPower The txPower of the beacon.
159
+ * @param rssi The RSSI of the beacon, subtract or add to this value to
160
+ * tune the dBm strength. 41dBm is subtracted from this value in the
161
+ * distance algorithm used by calculateAccuracy.
162
+ * @return Distance in meters, or null if unable to compute distance
163
+ * (occurs for example when txPower or rssi is undefined).
164
+ * @example
165
+ * // Note that beacon.txPower and beacon.rssi many be undefined,
166
+ * // in which case calculateAccuracy returns null. This happens
167
+ * // before txPower and rssi have been reported by the beacon.
168
+ * var distance = evothings.eddystone.calculateAccuracy(
169
+ * beacon.txPower, beacon.rssi);
170
+ */
171
+ evothings . eddystone . calculateAccuracy = function ( txPower , rssi )
172
+ {
173
+ if ( ! rssi || rssi >= 0 || ! txPower )
174
+ {
175
+ return null
176
+ }
177
+
178
+ // Algorithm
179
+ // http://developer.radiusnetworks.com/2014/12/04/fundamentals-of-beacon-ranging.html
180
+ // http://stackoverflow.com/questions/21338031/radius-networks-ibeacon-ranging-fluctuation
181
+
182
+ // The beacon distance formula uses txPower at 1 meters, but the Eddystone
183
+ // protocol reports the value at 0 meters. 41dBm is the signal loss that
184
+ // occurs over 1 meter, so we subtract that from the reported txPower.
185
+ var ratio = rssi * 1.0 / ( txPower - 41 )
186
+ if ( ratio < 1.0 )
187
+ {
188
+ return Math . pow ( ratio , 10 )
189
+ }
190
+ else
191
+ {
192
+ var accuracy = ( 0.89976 ) * Math . pow ( ratio , 7.7095 ) + 0.111
193
+ return accuracy
194
+ }
195
+ }
196
+
91
197
// Return true on frame type recognition, false otherwise.
92
- function parseFrameUID ( device , data , win , fail ) {
198
+ function parseFrameUID ( device , data , win , fail )
199
+ {
93
200
if ( data [ 0 ] != 0x00 ) return false ;
201
+
94
202
// The UID frame has 18 bytes + 2 bytes reserved for future use
95
203
// https://github.com/google/eddystone/tree/master/eddystone-uid
96
204
// Check that we got at least 18 bytes.
97
- if ( data . byteLength < 18 ) {
205
+ if ( data . byteLength < 18 )
206
+ {
98
207
fail ( "UID frame: invalid byteLength: " + data . byteLength ) ;
99
208
return true ;
100
209
}
210
+
101
211
device . txPower = evothings . util . littleEndianToInt8 ( data , 1 ) ;
102
212
device . nid = data . subarray ( 2 , 12 ) ; // Namespace ID.
103
213
device . bid = data . subarray ( 12 , 18 ) ; // Beacon ID.
214
+
104
215
win ( device ) ;
216
+
105
217
return true ;
106
218
}
107
219
108
- function parseFrameURL ( device , data , win , fail ) {
220
+ function parseFrameURL ( device , data , win , fail )
221
+ {
109
222
if ( data [ 0 ] != 0x10 ) return false ;
110
- if ( data . byteLength < 4 ) {
223
+
224
+ if ( data . byteLength < 4 )
225
+ {
111
226
fail ( "URL frame: invalid byteLength: " + data . byteLength ) ;
112
227
return true ;
113
228
}
229
+
114
230
device . txPower = evothings . util . littleEndianToInt8 ( data , 1 ) ;
115
- var url ;
231
+
116
232
// URL scheme prefix
233
+ var url ;
117
234
switch ( data [ 2 ] ) {
118
235
case 0 : url = 'http://www.' ; break ;
119
236
case 1 : url = 'https://www.' ; break ;
120
237
case 2 : url = 'http://' ; break ;
121
238
case 3 : url = 'https://' ; break ;
122
239
default : fail ( "URL frame: invalid prefix: " + data [ 2 ] ) ; return true ;
123
240
}
241
+
124
242
// Process each byte in sequence.
125
243
var i = 3 ;
126
- while ( i < data . byteLength ) {
244
+ while ( i < data . byteLength )
245
+ {
127
246
var c = data [ i ] ;
128
247
// A byte is either a top-domain shortcut, or a printable ascii character.
129
- if ( c < 14 ) {
130
- switch ( c ) {
248
+ if ( c < 14 )
249
+ {
250
+ switch ( c )
251
+ {
131
252
case 0 : url += '.com/' ; break ;
132
253
case 1 : url += '.org/' ; break ;
133
254
case 2 : url += '.edu/' ; break ;
@@ -143,44 +264,64 @@ function parseFrameURL(device, data, win, fail) {
143
264
case 12 : url += '.biz' ; break ;
144
265
case 13 : url += '.gov' ; break ;
145
266
}
146
- } else if ( c < 32 || c >= 127 ) {
267
+ }
268
+ else if ( c < 32 || c >= 127 )
269
+ {
147
270
// Unprintables are not allowed.
148
271
fail ( "URL frame: invalid character: " + data [ 2 ] ) ;
149
272
return true ;
150
- } else {
273
+ }
274
+ else
275
+ {
151
276
url += String . fromCharCode ( c ) ;
152
277
}
153
278
154
279
i += 1 ;
155
280
}
281
+
282
+ // Set URL field of the device.
156
283
device . url = url ;
284
+
157
285
win ( device ) ;
286
+
158
287
return true ;
159
288
}
160
289
161
- function parseFrameTLM ( device , data , win , fail ) {
290
+ function parseFrameTLM ( device , data , win , fail )
291
+ {
162
292
if ( data [ 0 ] != 0x20 ) return false ;
163
- if ( data [ 1 ] != 0x00 ) {
293
+
294
+ if ( data [ 1 ] != 0x00 )
295
+ {
164
296
fail ( "TLM frame: unknown version: " + data [ 1 ] ) ;
297
+
165
298
return true ;
166
299
}
167
- if ( data . byteLength != 14 ) {
300
+
301
+ if ( data . byteLength != 14 )
302
+ {
168
303
fail ( "TLM frame: invalid byteLength: " + data . byteLength ) ;
304
+
169
305
return true ;
170
306
}
171
307
172
308
device . voltage = evothings . util . bigEndianToUint16 ( data , 2 ) ;
173
309
174
310
var temp = evothings . util . bigEndianToUint16 ( data , 4 ) ;
175
311
if ( temp == 0x8000 )
312
+ {
176
313
device . temperature = 0x8000 ;
314
+ }
177
315
else
316
+ {
178
317
device . temperature = evothings . util . bigEndianToInt16 ( data , 4 ) / 256.0 ;
318
+ }
179
319
180
320
device . adv_cnt = evothings . util . bigEndianToUint32 ( data , 6 ) ;
181
321
device . dsec_cnt = evothings . util . bigEndianToUint32 ( data , 10 ) ;
182
322
183
323
win ( device ) ;
324
+
184
325
return true ;
185
326
}
186
327
0 commit comments