intval3/src/ble/index.ts

291 lines
7.3 KiB
TypeScript

'use strict'
/** @module ble */
/** Bluetooth Low Energy module */
import { inherits } from 'util'
import { networkInterfaces, homedir } from 'os'
import { readFileSync, existsSync, writeFileSync } from 'fs-extra'
const log = require('../log')('ble')
import { Wifi } from '../wifi'
const wifi = new Wifi()
const DEVICE_NAME : string = typeof process.env.DEVICE_NAME !== 'undefined' ? process.env.DEVICE_NAME : 'intval3'
const SERVICE_ID : string = typeof process.env.SERVICE_ID !== 'undefined' ? process.env.SERVICE_ID : 'intval3_ble'
const CHAR_ID : string = typeof process.env.CHAR_ID !== 'undefined' ? process.env.CHAR_ID : 'intval3char'
const WIFI_ID : string = typeof process.env.WIFI_ID !== 'undefined' ? process.env.WIFI_ID : 'wifichar'
const NETWORK : any = networkInterfaces() //?type?
const MAC : string = getMac() || spoofMac()
//Give the device a unique device name, needs to be in env
process.env.BLENO_DEVICE_NAME += '_' + MAC
import bleno from 'bleno'
const { Characteristic } = bleno
let currentWifi : string = 'disconnected'
let currentAddr : string = null
let getState : Function
const chars : any[] = []
interface WifiInfo {
ssid : string
pwd : string
}
interface WifiResponse {
available : string[]
current : string
ip : string
}
function createChar(name : string, uuid : string, prop : string[], write : Function, read : Function) {
const characteristic : any = function () {
Characteristic.call(this, {
uuid,
properties: prop
})
}
inherits(characteristic, 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 : Function, onRead : Function) {
const permissions : string[] = ['read', 'write'];
createChar('intval3', CHAR_ID, permissions, onWrite, onRead)
createChar('wifi', WIFI_ID, permissions, onWifiWrite, onWifiRead)
}
async function onWifiWrite (data : any, offset : number) {
let result : any
let utf8 : string
let obj : WifiInfo = {} as WifiInfo
let ssid : string
let pwd : string
let psk : any
if (offset) {
log.warn(`Offset scenario`)
result = bleno.Characteristic.RESULT_ATTR_NOT_LONG
return result
}
utf8 = data.toString('utf8')
obj = JSON.parse(utf8)
ssid = obj.ssid
pwd = obj.pwd
log.info(`connecting to AP`, { ssid : ssid })
try {
psk = await wifi.createPSK(ssid, pwd)
} catch (err) {
log.error('Error hashing wifi password', err)
result = bleno.Characteristic.RESULT_UNLIKELY_ERROR
return result
}
try {
await wifi.setNetwork(ssid, psk.plaintext, psk.hash)
} catch (err) {
log.error('Error configuring wifi', err)
result = bleno.Characteristic.RESULT_UNLIKELY_ERROR
return result
}
currentWifi = ssid
currentAddr = getIp()
log.info(`Connected to AP`, { ssid, ip : currentAddr })
result = bleno.Characteristic.RESULT_SUCCESS
return result
}
async function onWifiRead (offset : number, callback : Function) {
let result : any = bleno.Characteristic.RESULT_SUCCESS
let wifiRes : WifiResponse = {} as WifiResponse
let data : any
let list : any
try {
list = await wifi.list()
} catch (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))
return 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 = homedir() + '/.intval3id'
let uuid
let UUIDPATH
let TMP
let MACTMP
let dashRe
delete require.cache[FSPATH]
if (existsSync(IDFILE)) {
return 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')
writeFileSync(IDFILE, MACTMP, 'utf8')
return MACTMP
}
function getIp () {
let addr = null
let ipv4
const ifaces = 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 (str : string) {
return str[0].toUpperCase() + str.slice(1)
}
type functionKeys = "_onRead" | "_onWrite";
/** Class representing the bluetooth interface */
class BLE {
listeners : any = {}
/**
* Establishes Bluetooth Low Energy services, accessible to process through this class
*
* @constructor
*/
constructor (bleGetState : Function) {
log.info('Starting bluetooth service')
getState = bleGetState
bleno.on('stateChange', (state : any ) => {
log.info('stateChange', { 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 : Error) => {
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 : any) => {
log.info('accept', { clientAddress })
})
bleno.on('disconnect', (clientAddress : any) => {
log.info('disconnect', { clientAddress })
})
this._refreshWifi()
}
private async _refreshWifi () {
let ssid : string
try {
ssid = await wifi.getNetwork() as string
} catch (err) {
return log.error('wifi.getNetwork', err)
}
currentWifi = ssid
currentAddr = getIp()
log.info('wifi.getNetwork', {ssid : ssid, ip : currentAddr })
}
private _onWrite (data : any, offset : number, withoutResponse : Function, callback : Function) {
let result : any = {}
let utf8 : string
let obj : any
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
if (obj.type && this.listeners[obj.type]) {
return this.listeners[obj.type](obj, () => {
callback(result)
})
} else {
return callback(result)
}
}
private _onRead (offset : number, callback : Function) {
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 : string, callback : Function) {
this.listeners[eventName] = callback
}
}
module.exports = BLE