254 lines
6.3 KiB
JavaScript
254 lines
6.3 KiB
JavaScript
'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) {
|
|
const result = bleno.Characteristic.RESULT_SUCCESS
|
|
const 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 |