Copy ble and wifi modules as .ts source. Begin refactor into typescript as is before addressing bug issues.
This commit is contained in:
parent
883187a591
commit
9d1163926f
|
@ -0,0 +1,254 @@
|
|||
'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, //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
|
|
@ -0,0 +1,209 @@
|
|||
'use strict'
|
||||
|
||||
const networkPattern = /network[\s\S]*?=[\s\S]*?{([\s\S]*?)}/gi
|
||||
const quoteRe = new RegExp('"', 'g')
|
||||
|
||||
const filePath = '/etc/wpa_supplicant/wpa_supplicant.conf'
|
||||
const reconfigure = '/sbin/wpa_cli reconfigure'
|
||||
const refresh = 'ip link set wlan0 down && ip link set wlan0 up'
|
||||
const iwlist = '/sbin/iwlist wlan0 scanning | grep "ESSID:"'
|
||||
const iwgetid = '/sbin/iwgetid'
|
||||
|
||||
const log = require('../log')('wifi')
|
||||
const exec = require('child_process').exec
|
||||
const fs = require('fs')
|
||||
|
||||
let _entry = null
|
||||
let _ssid = null
|
||||
let _cb = null
|
||||
|
||||
/** Class representing the wifi features */
|
||||
class Wifi {
|
||||
constructor () {
|
||||
|
||||
}
|
||||
/**
|
||||
* List available wifi access points
|
||||
*
|
||||
* @param {function} callback Function which gets invoked after list is returned
|
||||
*/
|
||||
list (callback) {
|
||||
exec(iwlist, (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
console.error(err)
|
||||
return callback(err)
|
||||
}
|
||||
const limit = 20;
|
||||
const lines = stdout.split('\n')
|
||||
let output = []
|
||||
let line
|
||||
let i = 0
|
||||
for (let l of lines) {
|
||||
line = l.replace('ESSID:', '').trim()
|
||||
if (line !== '""' && i < limit) {
|
||||
line = line.replace(quoteRe, '')
|
||||
output.push(line)
|
||||
}
|
||||
i++
|
||||
}
|
||||
output = output.filter(ap => {
|
||||
if (ap !== '') return ap
|
||||
})
|
||||
return callback(null, output)
|
||||
})
|
||||
}
|
||||
/**
|
||||
* (internal function) Invoked after config file is read,
|
||||
* then invokes file write on the config file
|
||||
*
|
||||
* @param {object} err (optional) Error object only present if problem reading config file
|
||||
* @param {string} data Contents of the config file
|
||||
*/
|
||||
_readConfigCb (err, data) {
|
||||
let parsed
|
||||
let current
|
||||
if (err) {
|
||||
console.error(err)
|
||||
return _cb(err)
|
||||
}
|
||||
parsed = this._parseConfig(data)
|
||||
current = parsed.find(network => {
|
||||
return network.ssid === _ssid
|
||||
})
|
||||
if (typeof current !== 'undefined') {
|
||||
data = data.replace(current.raw, _entry)
|
||||
} else {
|
||||
data += '\n\n' + _entry
|
||||
}
|
||||
_entry = null
|
||||
fs.writeFile(filePath, data, 'utf8', this._writeConfigCb.bind(this))
|
||||
}
|
||||
/**
|
||||
* (internal function) Invoked after config file is written,
|
||||
* then executes reconfiguration command
|
||||
*
|
||||
* @param {object} err (optional) Error object only present if problem writing config file
|
||||
*/
|
||||
_writeConfigCb (err) {
|
||||
if (err) {
|
||||
console.error(err)
|
||||
return _cb(err)
|
||||
}
|
||||
exec(reconfigure, this._reconfigureCb.bind(this))
|
||||
}
|
||||
/**
|
||||
* (internal function) Invoked after reconfiguration command is complete
|
||||
*
|
||||
* @param {object} err (optional) Error object only present if configuration command fails
|
||||
* @param {string} stdout Standard output from reconfiguration command
|
||||
* @param {string} stderr Error output from command if fails
|
||||
*/
|
||||
_reconfigureCb (err, stdout, stderr) {
|
||||
if (err) {
|
||||
console.error(err)
|
||||
return _cb(err)
|
||||
}
|
||||
console.log('Wifi reconfigured')
|
||||
exec(refresh, this._refreshCb.bind(this))
|
||||
}
|
||||
/**
|
||||
* (internal function) Invoked after wifi refresh command is complete
|
||||
*
|
||||
* @param {object} err (optional) Error object only present if refresh command fails
|
||||
* @param {string} stdout Standard output from refresh command
|
||||
* @param {string} stderr Error output from command if fails
|
||||
*/
|
||||
_refreshCb (err, stdout, stderr) {
|
||||
if (err) {
|
||||
console.error(err)
|
||||
return _cb(err)
|
||||
}
|
||||
console.log('Wifi refreshed')
|
||||
_cb(null, { ssid : _ssid })
|
||||
_cb = () => {}
|
||||
}
|
||||
_parseConfig (str) {
|
||||
const networks = []
|
||||
const lines = str.split('\n')
|
||||
let network = {}
|
||||
for (let line of lines) {
|
||||
if (line.substring(0, 9) === 'network={') {
|
||||
network = {}
|
||||
network.raw = line
|
||||
} else if (network.raw && line.indexOf('ssid=') !== -1) {
|
||||
network.ssid = line.replace('ssid=', '').trim().replace(quoteRe, '')
|
||||
if (network.raw) {
|
||||
network.raw += '\n' + line
|
||||
}
|
||||
} else if (network.raw && line.substring(0, 1) === '}') {
|
||||
network.raw += '\n' + line
|
||||
networks.push(network)
|
||||
network = {}
|
||||
} else if (network.raw) {
|
||||
network.raw += '\n' + line
|
||||
}
|
||||
}
|
||||
return networks
|
||||
}
|
||||
/**
|
||||
* Create sanitized wpa_supplicant.conf stanza for
|
||||
* configuring wifi without storing plaintext passwords
|
||||
* @example
|
||||
* network={
|
||||
* ssid="YOUR_SSID"
|
||||
* #psk="YOUR_PASSWORD"
|
||||
* psk=6a24edf1592aec4465271b7dcd204601b6e78df3186ce1a62a31f40ae9630702
|
||||
* }
|
||||
*
|
||||
* @param {string} ssid SSID of wifi network
|
||||
* @param {string} pwd Plaintext passphrase of wifi network
|
||||
* @param {function} callback Function called after psk hash is generated
|
||||
*/
|
||||
createPSK (ssid, pwd, callback) {
|
||||
const cmd = `wpa_passphrase '${ssid.replace(/'/g, `'\\''`)}' '${pwd.replace(/'/g, `'\\''`)}' | grep "psk="`
|
||||
let lines
|
||||
let hash
|
||||
let plaintext
|
||||
exec(cmd, (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
return callback(err)
|
||||
}
|
||||
lines = stdout.replace('#psk=', '').split('psk=')
|
||||
hash = lines[1]
|
||||
plaintext = lines[0]
|
||||
callback(null, hash.trim(), plaintext.trim())
|
||||
})
|
||||
}
|
||||
/**
|
||||
* Function which initializes the processes for adding a wifi access point authentication
|
||||
*
|
||||
* @param {string} ssid SSID of network to configure
|
||||
* @param {string} pwd Password of access point, plaintext to be masked
|
||||
* @param {string} hash Password/SSID of access point, securely hashed
|
||||
* @param {function} callback Function invoked after process is complete, or fails
|
||||
*/
|
||||
setNetwork (ssid, pwd, hash, callback) {
|
||||
let masked = pwd.split('').map(char => { return char !== '"' ? '*' : '"' }).join('')
|
||||
_entry = `network={\n\tssid="${ssid}"\n\t#psk=${masked}\n\tpsk=${hash}\n}\n`
|
||||
_cb = callback
|
||||
_ssid = ssid
|
||||
fs.readFile(filePath, 'utf8', this._readConfigCb.bind(this))
|
||||
}
|
||||
/**
|
||||
* Executes command which gets the currently connected network
|
||||
*
|
||||
* @param {function} callback Function which is invoked after command is completed
|
||||
*/
|
||||
getNetwork (callback) {
|
||||
let output
|
||||
exec(iwgetid, (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
return callback(err)
|
||||
}
|
||||
output = stdout.split('ESSID:')[1].replace(quoteRe, '').trim()
|
||||
callback(null, output)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new Wifi()
|
|
@ -13,7 +13,10 @@
|
|||
"rootDir" : "./src/",
|
||||
"paths" : {
|
||||
"log" : [ "./lib/log" ],
|
||||
"delay" : [ "./lib/delay"]
|
||||
"delay" : [ "./lib/delay" ],
|
||||
"intval" : [ "./lib/intval" ],
|
||||
"ble" : [ "./lib/ble" ],
|
||||
"wifi" : [ "./lib/wifi" ]
|
||||
}
|
||||
},
|
||||
"exclude" : [
|
||||
|
|
Loading…
Reference in New Issue