'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_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, //hardcoded across panels 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