UNPKG

bleacon-fork

Version:

A Node.js library for creating, discovering, and configuring iBeacons

414 lines (320 loc) 14 kB
var crypto = require('crypto'); var bignum = require('bignum'); var debug = require('debug')('estimote'); var NobleDevice = require('noble-device-fork'); var GENERIC_ACCESS_SERVICE_UUID = '1800'; var DEVICE_NAME_UUID = '2a00'; var ESTIMOTE_SERVICE_UUID = 'b9403000f5f8466eaff925556b57fe6d'; var MAJOR_UUID = 'b9403001f5f8466eaff925556b57fe6d'; var MINOR_UUID = 'b9403002f5f8466eaff925556b57fe6d'; var UUID_1_UUID = 'b9403003f5f8466eaff925556b57fe6d'; var UUID_2_UUID = 'b9403004f5f8466eaff925556b57fe6d'; var POWER_LEVEL_UUID = 'b9403011f5f8466eaff925556b57fe6d'; var ADVERTISEMENT_INTERVAL_UUID = 'b9403012f5f8466eaff925556b57fe6d'; var TEMPERATURE_UUID = 'b9403021f5f8466eaff925556b57fe6d'; var MOTION_UUID = 'b9403031f5f8466eaff925556b57fe6d'; var SERVICE_2_09_UUID = 'b9403032f5f8466eaff925556b57fe6d'; var SERVICE_2_10_UUID = 'b9403051f5f8466eaff925556b57fe6d'; var BATTERY_LEVEL_UUID = 'b9403041f5f8466eaff925556b57fe6d'; var SERVICE_CONFIGURATION_UUID = 'b9403051f5f8466eaff925556b57fe6d'; var EDDYSTONE_UID_NAMESPACE_UUID = 'b9403071f5f8466eaff925556b57fe6d'; var EDDYSTONE_UID_INSTANCE_UUID = 'b9403072f5f8466eaff925556b57fe6d'; var EDDYSTONE_URL_UUID = 'b9403073f5f8466eaff925556b57fe6d'; var AUTH_SERVICE_UUID = 'b9402000f5f8466eaff925556b57fe6d'; var AUTH_SERVICE_1_UUID = 'b9402001f5f8466eaff925556b57fe6d'; // auth seed var AUTH_SERVICE_2_UUID = 'b9402002f5f8466eaff925556b57fe6d'; // auth vector var VERSION_SERVICE_UUID = 'b9404000f5f8466eaff925556b57fe6d'; var FIRMWARE_VERSION_UUID = 'b9404001f5f8466eaff925556b57fe6d'; var HARDWARE_VERSION_UUID = 'b9404002f5f8466eaff925556b57fe6d'; var Estimote = function(peripheral) { NobleDevice.call(this, peripheral); this.manufacturerData = (peripheral.advertisement.manufacturerData ? peripheral.advertisement.manufacturerData.toString('hex') : null); var serviceData = peripheral.advertisement.serviceData[0].data; this.address = serviceData.slice(0, 6).toString('hex').match(/.{1,2}/g).reverse().join(':'); this.addressData = new Buffer(this.address.split(':').join(''), 'hex'); this.measuredPower = serviceData.readInt8(6); this.major = serviceData.readUInt16LE(7); this.minor = serviceData.readUInt16LE(9); this._peripheral.on('disconnect', this.onDisconnect.bind(this)); this._onMotionDataBinded = this.onMotionData.bind(this); }; NobleDevice.Util.inherits(Estimote, NobleDevice); Estimote.SCAN_DUPLICATES = true; Estimote.is = function(peripheral) { var localName = peripheral.advertisement.localName; return ( (localName === 'estimote' || localName === 'EST') && // original || "new" name peripheral.advertisement.serviceData !== undefined && peripheral.advertisement.serviceData.length && peripheral.advertisement.serviceData[0].uuid === '180a'); }; Estimote.prototype.toString = function() { return JSON.stringify({ uuid: this.uuid, address: this.address, manufacturerData: this.manufacturerData, major: this.major, minor: this.minor, measuredPower: this.measuredPower }); }; Estimote.prototype.pair = function(callback) { var base = 5; var exp = Math.round(Math.random() * 0xffffffff); var mod = 0xfffffffb; var sec = bignum(base).powm(exp, mod); this.writeAuthService1(sec, function(error) { if (error) { return callback(error); } this.readAuthService1(function(error, authService1Value) { if (error) { return callback(error); } sec = bignum(authService1Value).powm(exp, mod); var authService2Data = new Buffer(16); // fill in authService2Data with address authService2Data[0] = this.addressData[5]; authService2Data[1] = this.addressData[4]; authService2Data[2] = this.addressData[3]; authService2Data[3] = this.addressData[2]; authService2Data[4] = this.addressData[1]; authService2Data[5] = this.addressData[0]; authService2Data[6] = this.addressData[3]; authService2Data[7] = this.addressData[4]; authService2Data[8] = this.addressData[5]; authService2Data[9] = this.addressData[0]; authService2Data[10] = this.addressData[1]; authService2Data[11] = this.addressData[2]; authService2Data[12] = this.addressData[1]; authService2Data[13] = this.addressData[3]; authService2Data[14] = this.addressData[2]; authService2Data[15] = this.addressData[4]; // encrypt var fixedKeyHexString = (this._peripheral.advertisement.localName === 'EST') ? 'c54fc29163e4457b8a9ac9868e1b3a9a' : // "new" fixed key (v3) 'ff8af207013625c2d810097f20d3050f'; // original fixed key var key = new Buffer(fixedKeyHexString, 'hex'); var iv = new Buffer('00000000000000000000000000000000', 'hex'); var cipher = crypto.createCipheriv('aes128', key, iv); cipher.setAutoPadding(false); authService2Data = cipher.update(authService2Data); // fill in key with sec var secData = new Buffer(4); secData.writeUInt32BE(sec, 0); key[0] = secData[3]; key[1] = secData[2]; key[2] = secData[1]; key[3] = secData[0]; key[4] = secData[0]; key[5] = secData[1]; key[6] = secData[2]; key[7] = secData[3]; key[8] = secData[3]; key[9] = secData[0]; key[10] = secData[2]; key[11] = secData[1]; key[12] = secData[0]; key[13] = secData[3]; key[14] = secData[1]; key[15] = secData[2]; // decrypt var decipher = crypto.createDecipheriv('aes128', key, iv); decipher.setAutoPadding(false); authService2Data = decipher.update(authService2Data); this.writeAuthService2(authService2Data, function(error) { callback(error); }.bind(this)); }.bind(this)); }.bind(this)); }; Estimote.prototype.writeDeviceName = function(deviceName, callback) { this.writeStringCharacteristic(GENERIC_ACCESS_SERVICE_UUID, DEVICE_NAME_UUID, deviceName, callback); }; Estimote.prototype.readMajor = function(callback) { this.readUInt16LECharacteristic(ESTIMOTE_SERVICE_UUID, MAJOR_UUID, callback); }; Estimote.prototype.writeMajor = function(major, callback) { this.writeUInt16LECharacteristic(ESTIMOTE_SERVICE_UUID, MAJOR_UUID, major, callback); }; Estimote.prototype.readMinor = function(callback) { this.readUInt16LECharacteristic(ESTIMOTE_SERVICE_UUID, MINOR_UUID, callback); }; Estimote.prototype.writeMinor = function(minor, callback) { this.writeUInt16LECharacteristic(ESTIMOTE_SERVICE_UUID, MINOR_UUID, minor, callback); }; Estimote.prototype.readUuid1 = function(callback) { this.readDataCharacteristic(ESTIMOTE_SERVICE_UUID, UUID_1_UUID, function(error, data) { if (error) { return callback(error); } callback(null, data.toString('hex')); }); }; Estimote.prototype.writeUuid1 = function(uuid1, callback) { this.writeDataCharacteristic(ESTIMOTE_SERVICE_UUID, UUID_1_UUID, new Buffer(uuid1, 'hex'), callback); }; Estimote.prototype.readUuid2 = function(callback) { this.readDataCharacteristic(ESTIMOTE_SERVICE_UUID, UUID_2_UUID, function(error, data) { if (error) { return callback(error); } callback(null, data.toString('hex')); }); }; Estimote.prototype.writeUuid2 = function(uuid2, callback) { this.writeDataCharacteristic(ESTIMOTE_SERVICE_UUID, UUID_2_UUID, new Buffer(uuid2, 'hex'), callback); }; Estimote.prototype.readPowerLevel = function(callback) { this.readUInt8Characteristic(ESTIMOTE_SERVICE_UUID, POWER_LEVEL_UUID, function(error, rawLevel) { var POWER_LEVEL_MAPPER = { '-30': 1, '-20': 2, '-16': 3, '-12': 4, '-8': 5, '-4': 6, '0': 7, '4': 8 }; if (error) { return callback(error); } var powerLevel = POWER_LEVEL_MAPPER['' + rawLevel]; if (powerLevel === undefined) { powerLevel = 'unknown'; } callback(error, powerLevel, rawLevel); }.bind(this)); }; Estimote.prototype.writePowerLevel = function(powerLevel, callback) { if (powerLevel < 1) { powerLevel = 1; } else if (powerLevel > 8) { powerLevel = 8; } var POWER_LEVEL_MAPPER = { 1: -30, 2: -20, 3: -16, 4: -12, 5: -8, 6: -4, 7: 0, 8: 4 }; var rawLevel = POWER_LEVEL_MAPPER[powerLevel]; this.writeUInt8Characteristic(ESTIMOTE_SERVICE_UUID, POWER_LEVEL_UUID, rawLevel, callback); }; Estimote.prototype.readAdvertisementInterval = function(callback) { // 50 -> 0x5000 -> 80 // 200 -> 0x4001 -> 320 // 2000 -> 0x800c -> 3200 this.readUInt16LECharacteristic(ESTIMOTE_SERVICE_UUID, ADVERTISEMENT_INTERVAL_UUID, function(error, rawAdvertisementInterval) { if (error) { return callback(error); } var advertisementInterval = (rawAdvertisementInterval / 8) * 5; callback(null, advertisementInterval); }.bind(this)); }; Estimote.prototype.writeAdvertisementInterval = function(advertisementInterval, callback) { var rawAdvertisementInterval = (advertisementInterval / 5) * 8; this.writeUInt16LECharacteristic(ESTIMOTE_SERVICE_UUID, ADVERTISEMENT_INTERVAL_UUID, rawAdvertisementInterval, callback); }; Estimote.prototype.readTemperature = function(callback) { this.writeUInt16LECharacteristic(ESTIMOTE_SERVICE_UUID, TEMPERATURE_UUID, 0xffff, function(error) { if (error) { return callback(error); } this.readUInt16LECharacteristic(ESTIMOTE_SERVICE_UUID, TEMPERATURE_UUID, function(error, temperature) { if (error) { return callback(error); } callback(null, temperature / 256.0); }); }.bind(this)); }; Estimote.prototype.subscribeMotion = function(callback) { this.notifyCharacteristic(ESTIMOTE_SERVICE_UUID, MOTION_UUID, true, this._onMotionDataBinded, callback); }; Estimote.prototype.unsubscribeMotion = function(callback) { this.notifyCharacteristic(ESTIMOTE_SERVICE_UUID, MOTION_UUID, false, this._onMotionDataBinded, callback); }; Estimote.prototype.onMotionData = function(data) { this.emit('motionStateChange', data.readUInt8(0) ? true : false); }; Estimote.prototype.readService2_9 = function(callback) { this.readDataCharacteristic(ESTIMOTE_SERVICE_UUID, SERVICE_2_09_UUID, callback); }; Estimote.prototype.readService2_10 = function(callback) { this.readDataCharacteristic(ESTIMOTE_SERVICE_UUID, SERVICE_2_10_UUID, callback); }; Estimote.prototype.readBatteryLevel = function(callback) { this.readUInt8Characteristic(ESTIMOTE_SERVICE_UUID, BATTERY_LEVEL_UUID, callback); }; var SERVICE_CONFIGURATION_MAPPER = ['default', 'eddystone-uid', 'eddystone-url']; Estimote.prototype.readServiceConfiguration = function(callback) { this.readDataCharacteristic(ESTIMOTE_SERVICE_UUID, SERVICE_CONFIGURATION_UUID, function(error, value) { if (error) { return callback(error); } callback(null, SERVICE_CONFIGURATION_MAPPER[value[3] & 0x03]); }.bind(this)); }; Estimote.prototype.writeServiceConfiguration = function(serviceConfig, callback) { this.readDataCharacteristic(ESTIMOTE_SERVICE_UUID, SERVICE_CONFIGURATION_UUID, function(error, value) { if (error) { return callback(error); } value[3] &= 0xfc; value[3] |= SERVICE_CONFIGURATION_MAPPER.indexOf(serviceConfig); this.writeDataCharacteristic(ESTIMOTE_SERVICE_UUID, SERVICE_CONFIGURATION_UUID, value, callback); }.bind(this)); }; Estimote.prototype.readEddystoneUidNamespace = function(callback) { this.readDataCharacteristic(ESTIMOTE_SERVICE_UUID, EDDYSTONE_UID_NAMESPACE_UUID, function(error, data) { if (error) { return callback(error); } callback(null, data.toString('hex')); }); }; Estimote.prototype.writeEddystoneUidNamespace = function(uidNamespace, callback) { this.writeDataCharacteristic(ESTIMOTE_SERVICE_UUID, EDDYSTONE_UID_NAMESPACE_UUID, new Buffer(uidNamespace, 'hex'), callback); }; Estimote.prototype.readEddystoneUidInstance = function(callback) { this.readDataCharacteristic(ESTIMOTE_SERVICE_UUID, EDDYSTONE_UID_INSTANCE_UUID, function(error, data) { if (error) { return callback(error); } callback(null, data.toString('hex')); }); }; Estimote.prototype.writeEddystoneUidInstance = function(uidInstance, callback) { this.writeDataCharacteristic(ESTIMOTE_SERVICE_UUID, EDDYSTONE_UID_INSTANCE_UUID, new Buffer(uidInstance, 'hex'), callback); }; Estimote.prototype.readEddystoneUrl = function(callback) { this.readDataCharacteristic(ESTIMOTE_SERVICE_UUID, EDDYSTONE_URL_UUID, callback); }; Estimote.prototype.writeEddystoneUrl = function(url, callback) { this.writeDataCharacteristic(ESTIMOTE_SERVICE_UUID, EDDYSTONE_URL_UUID, url, callback); }; Estimote.prototype.readAuthService1 = function(callback) { this.readUInt32LECharacteristic(AUTH_SERVICE_UUID, AUTH_SERVICE_1_UUID, callback); }; Estimote.prototype.writeAuthService1 = function(value, callback) { this.writeUInt32LECharacteristic(AUTH_SERVICE_UUID, AUTH_SERVICE_1_UUID, value, callback); }; Estimote.prototype.readAuthService2 = function(callback) { this.readDataCharacteristic(AUTH_SERVICE_UUID, AUTH_SERVICE_2_UUID, callback); }; Estimote.prototype.writeAuthService2 = function(data, callback) { this.writeDataCharacteristic(AUTH_SERVICE_UUID, AUTH_SERVICE_2_UUID, data, callback); }; Estimote.prototype.readFirmwareVersion = function(callback) { this.readStringCharacteristic(VERSION_SERVICE_UUID, FIRMWARE_VERSION_UUID, callback); }; Estimote.prototype.readHardwareVersion = function(callback) { this.readStringCharacteristic(VERSION_SERVICE_UUID, HARDWARE_VERSION_UUID, callback); }; module.exports = Estimote;