-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathbtleblinkup.device.lib.nut
691 lines (618 loc) · 24.8 KB
/
btleblinkup.device.lib.nut
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
// MIT License
//
// Copyright (c) 2019 Electric Imp, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
/**
* @constant {integer} BTLE_BLINKUP_WIFI_SCAN_INTERVAL - The interval between separate WiFi network scans.
*
*/
const BTLE_BLINKUP_WIFI_SCAN_INTERVAL = 120;
/**
* Squirrel class for providing BlinkUp services via Bluetooth LE on a compatible imp.
*
* Bus UART
* Availibility Device
* @author Tony Smith (@smittytone)
* @license MIT
*
* @class
*/
class BTLEBlinkUp {
/**
* @property {string} VERSION - The library version.
*
*/
static VERSION = "2.0.0";
/**
* @property {imp::bluetooth} ble - The imp API hardware.bluetooth instance.
*
*/
ble = null;
/**
* @property {string} agentURL - The URL of the device's agent.
*
*/
agentURL = null;
// ********** Private instance properties **********
_uuids = null;
_blinkup = null;
_incoming = null;
_incomingCB = null;
_networks = null;
_pin_LPO_IN = null;
_pin_BT_REG_ON = null;
_uart = null;
_blinking = false;
_scanning = false;
_impType = null;
/**
* Instantiate the BLE BlinkUp Class.
* NOTE lpoPin, regonPin and uart only required by imp004m.
*
* @constructor
*
* @param {array} uuids - Table of UUID service values (see Read Me).
* @param {string} [firmware] - The Bluetooth radio firmware.
* @param {imp::pin} [lpoPin] - The imp004m module pin object connected to the BLE modele's LPO pin. Default: hardware.pinE.
* @param {imp::pin} [regonPin] - The imp004m module pin object connected to the BLE modele's REG_ON pin. Default: hardware.pinJ.
* @param {imp::uart} [uart] - The configured imp004m UART bus to which the BLE module is connected. Default: hardware.uartFGJH.
*
* @returns {instance} The instance.
*
*/
constructor(uuids = null, firmware = null, lpoPin = null, regonPin = null, uart = null) {
// Check that we have recieved firmware
if (firmware == null) throw "BTLEBlinkUp() requires Blueooth firmware supplied as a string or blob.";
// Apply the BlinkUp service's UUIDs, or the defaults if none are provided
if (uuids == null || typeof uuids != "table" || uuids.len() != 8) throw "BTLEBlinkUp() requires service UUIDs to be provided as a table";
if (!_checkUUIDs(uuids)) throw "BTLEBlinkUp() requires the service UUID table to contain specific key names";
_uuids = uuids;
_impType = imp.info().type;
if (_impType == "imp004m") {
// Set the BLE radio pins, either to the passed in values, or the defaults
// Defaults to the imp004m Breakout Board
_pin_LPO_IN = lpoPin != null ? lpoPin : hardware.pinE;
_pin_BT_REG_ON = regonPin != null ? regonPin : hardware.pinJ;
_uart = uart != null ? uart : hardware.uartFGJH;
} else {
_pin_LPO_IN = null;
_pin_BT_REG_ON = null;
_uart = null;
}
// Initialize the radio
_init(firmware);
}
/**
* Listen for a BlinkUp request.
*
* This is a convenience method for serving BlinkUp. It assumes that you have already specified the required level of security,
* using setSecurity(). It uses default values for advertising, min. and max. interval values, and serves only the BlinkUp
* and Device Information services.
*
* @param {string|blob} [advert] - Optional BLE advertisement. Default: library-set advert.
* @param {function} [callback] - Optional 'on connected' callback function. Default: null.
*
*/
function listenForBlinkUp(advert = null, callback = null) {
serve();
onConnect(callback);
advertise(advert);
}
/**
* Set Bluetooth LE security mode.
*
* Specify the Bluetooth LE security mode (and PIN) as per the imp API method bluetooth.setsecurity().
* It will default to no security (mode 1) in case of error.
* NOTE This needs to be run separately from listenForBlinkUp().
*
* @param {integer} mode - BLE security mode integer: 1, 3 or 4.
* @param {string|integer} [pin] - Optional 'on connected' callback function. Default: "000000".
*
* @returns {integer} The mode selected.
*
*/
function setSecurity(mode = 1, pin = "000000") {
// Check for a valid Bluetooth instance
if (ble == null) {
server.error("BTLEBlinkUp.setSecurity() - Bluetooth LE not initialized");
return 1;
}
// Check that a valid mode has been provided
if (mode != 1 && mode != 3 && mode != 4) {
server.error("BTLEBlinkUp.setSecurity() - undefined security mode selected");
ble.setsecurity(1);
return 1;
}
// Check that a PIN has been provided for modes 3 and 4
if (pin == null && mode > 1) {
server.error("BTLEBlinkUp.setSecurity() - security modes 3 and 4 require a PIN");
ble.setsecurity(1);
return 1;
}
// Parameter 'pin' should be a string or an integer and no more than six digits
if (typeof pin == "string") {
if (pin.len() > 6) {
server.error("BTLEBlinkUp.setSecurity() - security PIN cannot be more than six characters");
ble.setsecurity(1);
return 1;
}
try {
pin = pin.tointeger();
} catch (err) {
server.error("BTLEBlinkUp.setSecurity() - security PIN must contain only decimal numeric characters");
ble.setsecurity(1);
return 1;
}
} else if (typeof pin == "integer") {
if (pin < 0 || pin > 999999) {
server.error("BTLEBlinkUp.setSecurity() - security PIN must contain 1 to 6 digits");
ble.setsecurity(1);
return 1;
}
} else {
server.error("BTLEBlinkUp.setSecurity() - security PIN must be a string or integer");
ble.setsecurity(1);
return 1;
}
if (mode == 1) {
// Ignore the pin as it's not needed
ble.setsecurity(1);
} else {
ble.setsecurity(mode, pin);
}
return mode;
}
/**
* Set the Agent URL of the host device.
*
* This is included in the device info service data.
*
* @param {string} url - The agent's URL string.
*
* @returns {string} The specified URL. Will be an empty string if the agent URL could not be set.
*
*/
function setAgentURL(url = "") {
agentURL = typeof url == "string" ? url : "";
return agentURL;
}
/**
* Set up the Bluetooth GATT server for BlinkUp.
*
* This always adds the BlinkUp service and standard Device Info service.
*
* @param {array} [otherServices] - An array of one or more services which you would like the device to
* provides **in addition** to BlinkUp and the standard Device Info service.
*
*/
function serve(otherServices = null) {
// Check for a valid Bluetooth instance
if (ble == null) {
server.error("BTLEBlinkUp.serve() - Bluetooth LE not initialized");
return;
}
// Define the BlinkUp service
local service = {};
service.uuid <- _uuids.blinkup_service_uuid;
service.chars <- [];
// Define the SSID setter characteristic
local chrx = {};
chrx.uuid <- _uuids.ssid_setter_uuid;
chrx.flags <- 0x08;
chrx.write <- function(conn, v) {
_blinkup.ssid = v.tostring();
_blinkup.updated = true;
server.log("WiFi SSID set");
return 0x0000;
}.bindenv(this);
service.chars.append(chrx);
// Define the password setter characteristic
chrx = {};
chrx.uuid <- _uuids.password_setter_uuid;
chrx.write <- function(conn, v) {
_blinkup.pwd = v.tostring();
_blinkup.updated = true;
server.log("WiFi password set");
return 0x0000;
}.bindenv(this);
service.chars.append(chrx);
// Define the Plan ID setter characteristic
chrx = {};
chrx.uuid <- _uuids.planid_setter_uuid;
chrx.write <- function(conn, v) {
_blinkup.planid = v.tostring();
_blinkup.updated = true;
server.log("Plan ID set");
return 0x0000;
}.bindenv(this);
service.chars.append(chrx);
// Define the Enrollment Token setter characteristic
chrx = {};
chrx.uuid <- _uuids.token_setter_uuid;
chrx.write <- function(conn, v) {
_blinkup.token = v.tostring();
_blinkup.updated = true;
server.log("Enrolment Token set");
return 0x0000;
}.bindenv(this);
service.chars.append(chrx);
// Define a dummy setter characteristic to trigger the imp restart
chrx = {};
chrx.uuid <- _uuids.blinkup_trigger_uuid;
chrx.write <- function(conn, v) {
if (_blinkup.updated) {
server.log("Device Activation triggered");
_blinkup.update();
return 0x0000;
} else {
return 0x1000;
}
}.bindenv(this);
service.chars.append(chrx);
// Define a dummy setter characteristic to trigger WiFi clearance
chrx = {};
chrx.uuid <- _uuids.wifi_clear_trigger_uuid;
chrx.write <- function(conn, v) {
server.log("Device WiFi clearance triggered");
_blinkup.clear();
return 0x0000;
}.bindenv(this);
service.chars.append(chrx);
// Define the getter characteristic that serves the list of nearby WLANs
chrx = {};
chrx.uuid <- _uuids.wifi_getter_uuid;
chrx.read <- function(conn) {
// There's no http.jsonencode() on the device so stringify the key data
// Networks are stored as "ssid[newline]open/secure[newline][newline]"
// NOTE set _blinking to true so we don't asynchronously update the list
// of networks while also using it here
server.log("Sending WLAN list to app");
local ns = "";
_blinking = true;
for (local i = 0 ; i < _networks.len() ; i++) {
local network = _networks[i];
ns += (network["ssid"] + "\n");
ns += ((network["open"] ? "unlocked" : "locked") + "\n\n");
}
_blinking = false;
// Remove the final two newlines
ns = ns.slice(0, ns.len() - 2);
return ns;
}.bindenv(this);
service.chars.append(chrx);
// Offer the service we have just defined
local services = [];
services.append(service);
// Device information service
service = { "uuid": 0x180A,
"chars": [
{ "uuid": 0x2A29, "value": "Electric Imp" }, // manufacturer name
{ "uuid": 0x2A25, "value": hardware.getdeviceid() }, // serial number
{ "uuid": 0x2A24, "value": imp.info().type }, // model number
{ "uuid": 0x2A23, "value": (agentURL != null ? agentURL : "null") }, // system ID (agent ID)
{ "uuid": 0x2A26, "value": imp.getsoftwareversion() }] // firmware version
};
services.append(service);
if (otherServices != null) {
if (typeof otherServices == "array") {
services.extend(otherServices);
} else if (typeof otherServices == "table") {
services.append(otherServices);
}
}
ble.servegatt(services);
}
/**
* Begin advertising the device.
*
* NOTE If no argument is passed in to 'advert', the library will build one of its own based on the BlinkUp service,
* but this will leave the device unnamed.
*
* @param {blob|string} advert - A BLE advertisement packet. Must be 31 bytes or less. We do not check that the data is valid.
* @param {integer} [max] - Optional maximum interval in ms. Default: 100.
* @param {integer} [min] - Optional minimum interval in ms. Default: 100.
*
*/
function advertise(advert = null, min = 100, max = 100) {
// Check for a valid Bluetooth instance
if (ble == null) {
server.error("BTLEBlinkUp.advertise() - Bluetooth LE not initialized");
return;
}
// Check the 'min' and 'max' values
if (min < 0 || min > 100) min = 100;
if (max < 0 || max > 100) max = 100;
if (min > max) {
// Swap 'min' and 'max' around if 'min' is bigger than 'max'
local a = max;
max = min;
min = a;
}
// Advertise the supplied advert then exit
if (advert != null) {
if (typeof advert != "blob" && typeof advert != "string") {
server.error("BTLEBlinkUp.advertise() - Misformed advertisement provided");
return;
}
if (advert.len() > 31) {
server.error("BTLEBlinkUp.advertise() - Advertisement data too long (31 bytes max.)");
return;
}
ble.startadvertise(advert, min, max);
return;
}
// Otherwise build the advert packed based on the service UUID
// NOTE We need to reverse the octet order for transmission
local ss = _uuids.blinkup_service_uuid;
local ns = imp.info().type;
local ab = blob(ss.len() / 2 + ns.len() + 4);
ab.seek(0, 'b');
// Write in the BlinkUp service UUID:
// Byte 0 - The data length
ab.writen(ss.len() / 2 + 1, 'b');
// Byte 1 - The data type flag (0x07)
ab.writen(7, 'b');
// Bytes 2+ — The UUID in little endian
local maxs = ss.len() - 2;
for (local i = 0 ; i < maxs + 2 ; i += 2) {
local bs = ss.slice(maxs - i, maxs - i + 2)
ab.writen(_hexStringToInt(bs), 'b');
}
// Write in the device name
// Byte 0 - The length
ab.writen(ns.len() + 1, 'b');
// Byte 1 - The data type flag (0x09)
ab.writen(9, 'b');
// Bytes 2+ - The imp type as its name
foreach (ch in ns) ab.writen(ch, 'b');
ble.startadvertise(ab, min, max);
}
/**
* The onConnect callback
*
* @callback onConnect
*
* @param {imp::bluetoothconnection} conn - The connection's imp API BluetoothConnection instance.
* @param {string} address - The connection's address.
* @param {integer} security - The security mode of the connection (1, 3 or 4).
* @param {string} state - The state of the connection: "connnected" or "disconnected".
*
*/
/**
* Register the host app's connection/disconnection notification callback.
*
* NOTE If no argument is passed in to 'advert', the library will build one of its own based on the BlinkUp service,
* but this will leave the device unnamed.
*
* @param {onConnect} callback - A function in the host app to handle connection events.
*
*/
function onConnect(callback = null) {
// Check for a valid Bluetooth instance
if (ble == null) {
server.error("BTLEBlinkUp.onConnect() - Bluetooth LE not initialized");
return;
}
// Check for a valid connection/disconnection notification callback
if (callback == null || typeof callback != "function") {
server.error("BTLEBlinkUp.onConnect() requires a non-null callback");
return;
}
// Store the host app's callback...
_incomingCB = callback;
// ...which will be triggered by the library's own
// connection callback, _connectHandler()
ble.onconnect(_connectHandler.bindenv(this));
}
// ********** PRIVATE FUNCTIONS - DO NOT CALL **********
/**
* Boot up the Bluetooth radio: set up the power lines via GPIO.
*
* @private
*
*/
function _init(firmware) {
// FROM 2.0.0, support imp006 by partitioning imp004m-specific settings
if (_impType == "imp004m") {
// NOTE These require a suitably connected module - we can't check for that here
_pin_LPO_IN.configure(DIGITAL_OUT, 0);
_pin_BT_REG_ON.configure(DIGITAL_OUT, 1);
}
// Scan for WiFi networks around the device
local now = hardware.millis();
_scan(false);
// Set up the incoming data structure which includes a function to trigger
// that handles the application of the received data
_blinkup = {};
_blinkup.ssid <- "";
_blinkup.pwd <- "";
_blinkup.planid <- "";
_blinkup.token <- "";
_blinkup.updated <- false;
_blinkup.update <- function() {
// Apply the received data
// TODO check for errors
// Close the existing connection to the mobile app
if (_incoming != null) _incoming.close();
_blinking = true;
// Disconnect from the server
server.flush(10);
server.disconnect();
// Apply the new WiFi details
imp.setwificonfiguration(_blinkup.ssid, _blinkup.pwd);
if (_blinkup.planid != "" && _blinkup.token != "") {
// Only write the plan ID and enrollment token if they have been set
// NOTE This is to support WiFi-only BlinkUp
imp.setenroltokens(_blinkup.planid, _blinkup.token);
}
// Inform the host app about activation - it may use this, eg. to
// write a 'has activated' signature to the SPI flash
local data = { "activated": true };
_incomingCB(data);
// Reboot the imp upon idle
// (to allow writes to flash time to take place, etc.)
imp.onidle(function() {
imp.reset();
}.bindenv(this));
}.bindenv(this);
_blinkup.clear <- function() {
// Close the existing connection to the mobile app
if (_incoming != null) _incoming.close();
// Clear the WiFi settings ONLY - this will affect the next
// disconnection/connection cycle, not the current connection
imp.clearconfiguration(CONFIG_WIFI);
}.bindenv(this);
// We need to wait 0.01s for the BLE radio to boot, so see how
// long the set-up took before sleeping (which may not be needed)
now = hardware.millis() - now;
if (now < 10) imp.sleep((10 - now) / 1000);
try {
// Instantiate Bluetooth LE
// FROM 2.0.0 - use separate calls for imp004m and imp006
ble = _impType == "imp004m" ? hardware.bluetooth.open(_uart, firmware) : hardware.bluetooth.open(firmware);
} catch (err) {
throw "BLE failed to initialize (error: " + err + ")";
}
}
/**
* This is the library's own handler for incoming connections.
*
* It calls the host app as required upon connection.
* Issues data via the onConnect callback, if one has been registered.
*
* @private
*
* @param {imp::bluetoothconnection} conn - The connection's imp API BluetoothConnection instance.
*
*/
function _connectHandler(conn) {
if (_incomingCB == null) return;
// Save the connecting device's BluetoothConnection instance
_incoming = conn;
// Register the library's own onclose handler
conn.onclose(_closeHandler.bindenv(this));
// Package up the connection data for return to the host app
local data = { "conn": conn,
"address": conn.address(),
"security": conn.security(),
"state": "connected" };
// Call the host app's onconnect handler
_incomingCB(data);
}
/**
* This is the library's own handler for broken connections.
*
* It calls the host app as required upon disconnection.
* This will never be called if the host app did not provide onConnect() with a notification callback.
* Issues data via the onConnect callback, if one has been registered.
*
* @private
*
* @param {imp::bluetoothconnection} conn - The connection's imp API BluetoothConnection instance.
*
*/
function _closeHandler() {
if (_incomingCB == null) return;
// Package up the connection data for return to the host app
local data = { "conn": _incoming,
"address": _incoming.address(),
"state": "disconnected" };
// Call the host app's onconnect handler
_incomingCB(data);
}
/**
* Convert a hex string to an integer.
*
* @private
*
* @param {string} hs - A string in hexadecimal format.
*
* @returns {integer} The integer that that source string represents.
*
*/
function _hexStringToInt(hs) {
local i = 0;
foreach (c in hs) {
local n = c - '0';
if (n > 9) n = ((n & 0x1F) - 7);
i = (i << 4) + n;
}
return i;
}
/**
* Scan for nearby WiFi networks compatible with the host imp.
*
* Sets the instance's internal list of nearby networks, '_networks'.
*
* @private
*
* @param {bool} [shouldLoop] - Whether we should queue up a repeat scan. Default: false.
*
*/
function _scan(shouldLoop = false) {
// Make sure we're not sending BlinkUp data
if (!_blinking) {
_networks = imp.scanwifinetworks();
// Check the list of WLANs for networks which have multiple reachable access points,
// ie. networks of the same SSID but different BSSIDs, otherwise the same WLAN will
// be listed twice
local i = 0;
do {
local network = _networks[i];
i++;
for (local j = 0 ; j < _networks.len() ; j++) {
local aNetwork = _networks[j];
if (network.ssid == aNetwork.ssid && network.bssid != aNetwork.bssid) {
// We have two identical SSIDs but different base stations, so remove one
_networks.remove(j);
}
}
} while (_networks.len() > i);
}
// Should we schedule a network list refresh?
if (shouldLoop) {
// Yes, we should, after 'BTLE_BLINKUP_WIFI_SCAN_INTERVAL' seconds
imp.wakeup(BTLE_BLINKUP_WIFI_SCAN_INTERVAL, function() {
_scan(true);
}.bindenv(this));
}
}
/**
* Check that the table of UUIDs supplied by the constructor has the correct keys.
*
* @private
*
* @param {table} [uuids] - The supplied Bluetooth service UUIDs.
*
* @returns {bool} Whether the table has the correct key names (true) or not (false).
*
*/
function _checkUUIDs(uuids) {
// Make sure the UUIDs table contains the correct keys, which are:
local keyList = ["blinkup_service_uuid", "ssid_setter_uuid", "password_setter_uuid",
"planid_setter_uuid", "token_setter_uuid", "blinkup_trigger_uuid",
"wifi_getter_uuid", "wifi_clear_trigger_uuid"];
local got = 0;
foreach (key in keyList) {
if (uuids[key].len() != null) got++;
}
return got == 8 ? true : false;
}
}