'use strict'; /** @module ble */ /** Bluetooth Low Energy module */ const util = require('util'); const os = require('os'); const log = require('../log')('ble'); const wifi = require('../wifi'); const DEVICE_NAME = process.env.DEVICE_NAME || 'intval3'; const SERVICE_ID = process.env.SERVICE_ID || 'intval3_ble'; const CHAR_ID = process.env.CHAR_ID || 'intval3char'; const WIFI_ID = process.env.WIFI_ID || 'wifichar'; const NETWORK = os.networkInterfaces(); const MAC = getMac() || spoofMac(); //Give the device a unique device name, needs to be in env process.env.BLENO_DEVICE_NAME += '_' + MAC; const bleno = require('bleno'); let currentWifi = 'disconnected'; let currentAddr = null; let getState; const chars = []; function createChar(name, uuid, prop, write, read) { function characteristic() { bleno.Characteristic.call(this, { uuid: uuid, properties: prop }); } util.inherits(characteristic, bleno.Characteristic); if (prop.indexOf('read') !== -1) { //data, offset, withoutResponse, callback characteristic.prototype.onReadRequest = read; } if (prop.indexOf('write') !== -1) { characteristic.prototype.onWriteRequest = write; } chars.push(new characteristic()); } function createChars(onWrite, onRead) { createChar('intval3', CHAR_ID, ['read', 'write'], onWrite, onRead); createChar('wifi', WIFI_ID, ['read', 'write'], onWifiWrite, onWifiRead); } function onWifiWrite(data, offset, withoutResponse, callback) { let result; let utf8; let obj; let ssid; let pwd; if (offset) { log.warn(`Offset scenario`); result = bleno.Characteristic.RESULT_ATTR_NOT_LONG; return callback(result); } utf8 = data.toString('utf8'); obj = JSON.parse(utf8); ssid = obj.ssid; pwd = obj.pwd; log.info(`connecting to AP`, { ssid: ssid }); return wifi.createPSK(ssid, pwd, (err, hash, plaintext) => { if (err) { log.error('Error hashing wifi password', err); result = bleno.Characteristic.RESULT_UNLIKELY_ERROR; return callback(result); } return wifi.setNetwork(ssid, plaintext, hash, (err, data) => { if (err) { log.error('Error configuring wifi', err); result = bleno.Characteristic.RESULT_UNLIKELY_ERROR; return callback(result); } currentWifi = ssid; currentAddr = getIp(); log.info(`Connected to AP`, { ssid: ssid, ip: currentAddr }); result = bleno.Characteristic.RESULT_SUCCESS; return callback(result); }); }); } function onWifiRead(offset, callback) { let result = bleno.Characteristic.RESULT_SUCCESS; let wifiRes = {}; let data; wifi.list((err, list) => { if (err) { result = bleno.Characteristic.RESULT_UNLIKELY_ERROR; return callback(result); } wifiRes.available = list; wifiRes.current = currentWifi; wifiRes.ip = currentAddr; log.info('Discovered available APs', { found: list.length }); data = new Buffer(JSON.stringify(wifiRes)); callback(result, data.slice(offset, data.length)); }); } function getMac() { const colonRe = new RegExp(':', 'g'); if (NETWORK && NETWORK.wlan0 && NETWORK.wlan0[0] && NETWORK.wlan0[0].mac) { return NETWORK.wlan0[0].mac.replace(colonRe, ''); } return undefined; } function spoofMac() { const fs = require('fs'); const FSPATH = require.resolve('uuid'); const IDFILE = os.homedir() + '/.intval3id'; let uuid; let UUIDPATH; let TMP; let MACTMP; let dashRe; delete require.cache[FSPATH]; if (fs.existsSync(IDFILE)) { return fs.readFileSync(IDFILE, 'utf8'); } uuid = require('uuid').v4; UUIDPATH = require.resolve('uuid'); delete require.cache[UUIDPATH]; TMP = uuid(); MACTMP = TMP.replace(dashRe, '').substring(0, 12); dashRe = new RegExp('-', 'g'); fs.writeFileSync(IDFILE, MACTMP, 'utf8'); return MACTMP; } function getIp() { let addr = null; let ipv4; const ifaces = os.networkInterfaces(); if (ifaces && ifaces.wlan0) { ipv4 = ifaces.wlan0.filter(iface => { if (iface.family === 'IPv4') { return iface; } }); if (ipv4.length === 1) { addr = ipv4[0].address; } } return addr; } function capitalize(s) { return s[0].toUpperCase() + s.slice(1); } /** Class representing the bluetooth interface */ class BLE { /** * Establishes Bluetooth Low Energy services, accessible to process through this class * * @constructor */ constructor(bleGetState) { log.info('Starting bluetooth service'); getState = bleGetState; bleno.on('stateChange', state => { log.info('stateChange', { state: state }); if (state === 'poweredOn') { log.info('Starting advertising', { DEVICE_NAME, DEVICE_ID: process.env.BLENO_DEVICE_NAME }); bleno.startAdvertising(DEVICE_NAME, [CHAR_ID]); } else { bleno.stopAdvertising(); } }); bleno.on('advertisingStart', err => { log.info('advertisingStart', { res: (err ? 'error ' + err : 'success') }); createChars(this._onWrite.bind(this), this._onRead.bind(this)); if (!err) { bleno.setServices([ new bleno.PrimaryService({ uuid: SERVICE_ID, characteristics: chars }) ]); } }); bleno.on('accept', clientAddress => { log.info('accept', { clientAddress: clientAddress }); }); bleno.on('disconnect', clientAddress => { log.info('disconnect', { clientAddress: clientAddress }); }); wifi.getNetwork((err, ssid) => { if (err) { return log.error('wifi.getNetwork', err); } currentWifi = ssid; currentAddr = getIp(); log.info('wifi.getNetwork', { ssid: ssid, ip: currentAddr }); }); } _onWrite(data, offset, withoutResponse, callback) { let result = {}; let utf8; let obj; let fn; if (offset) { log.warn(`Offset scenario`); result = bleno.Characteristic.RESULT_ATTR_NOT_LONG; return callback(result); } utf8 = data.toString('utf8'); obj = JSON.parse(utf8); result = bleno.Characteristic.RESULT_SUCCESS; fn = `_on${capitalize(obj.type)}`; if (obj.type && this[fn]) { return this[fn](obj, () => { callback(result); }); } else { return callback(result); } } _onRead(offset, callback) { const result = bleno.Characteristic.RESULT_SUCCESS; const state = getState(); const data = new Buffer(JSON.stringify(state)); callback(result, data.slice(offset, data.length)); } /** * Binds functions to events that are triggered by BLE messages * * @param {string} eventName Name of the event to to bind * @param {function} callback Invoked when the event is triggered */ on(eventName, callback) { this[`_on${capitalize(eventName)}`] = callback; } } module.exports = BLE; //# sourceMappingURL=index.js.map