rasp-blefi
Version:
Experimental project that allow headless configuration of WIFI network on Rasperry PI using BLE.
607 lines (517 loc) • 17.2 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var events = require('events');
var bleno = require('@abandonware/bleno');
var bleno__default = _interopDefault(bleno);
var utils = _interopDefault(require('util'));
var winston = require('winston');
var child_process = require('child_process');
function _inheritsLoose(subClass, superClass) {
subClass.prototype = Object.create(superClass.prototype);
subClass.prototype.constructor = subClass;
subClass.__proto__ = superClass;
}
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return self;
}
var _process$env$BLEFI_LO;
var env = /*#__PURE__*/((_process$env$BLEFI_LO = process.env['BLEFI_LOG']) !== null && _process$env$BLEFI_LO !== void 0 ? _process$env$BLEFI_LO : 'INFO').toLowerCase();
var silent = ! /*#__PURE__*/['trace', 'debug', 'info', 'warn', 'error'].includes(env);
var level = silent ? 'debug' : env;
var consoleOptions = {
level: level,
silent: silent,
format: /*#__PURE__*/winston.format.combine( /*#__PURE__*/winston.format.simple(), /*#__PURE__*/winston.format.timestamp())
};
var logger = /*#__PURE__*/winston.createLogger({
transports: [/*#__PURE__*/new winston.transports.Console(consoleOptions)]
});
/**
* BLE characteristic for Network connection status
* @summary Hold the current connection status of the network
* @param network - network
*/
function NetworkStatusCharacteristic(network) {
var _this = this;
this.network = network;
this._updateValueCb = null;
bleno__default.Characteristic.call(this, {
uuid: 'fff1',
properties: ['read', 'notify'],
descriptors: [new bleno__default.Descriptor({
uuid: '2901',
value: ''
})],
onReadRequest: function onReadRequest(_offset, cb) {
logger.info('bluetooth on read request network status characateristic');
_this.network.getStatus().then(function (status) {
return cb(_this.RESULT_SUCCESS, Buffer.from(status));
})["catch"](function (err) {
logger.error('unable to get network status', {
err: err
});
cb(_this.RESULT_UNLIKELY_ERROR);
});
},
onSubscribe: function onSubscribe(_maxValueSize, updateValueCb) {
logger.info('bluetooth on subscribe network status characateristic');
_this._updateValueCb = updateValueCb; // listen for status change
_this.network.on('statusChange', function (status) {
_this._updateValueCb(Buffer.from(status));
});
}
});
}
utils.inherits(NetworkStatusCharacteristic, bleno.Characteristic);
/**
* BLE characteristic for Network credential
* @param network - network
*/
function NetworkCredentialCharacteristic(network) {
var _this = this;
this.network = network;
bleno__default.Characteristic.call(this, {
uuid: 'fff2',
properties: ['write'],
descriptors: [new bleno__default.Descriptor({
uuid: '2901',
value: ''
})],
onWriteRequest: function onWriteRequest(data, offset, _withoutResponse, cb) {
logger.info('bluetooth read network credential characateristic');
if (offset) {
cb(_this.RESULT_ATTR_NOT_LONG);
} else if (data.length > 32 || data.length <= 0) {
cb(_this.RESULT_INVALID_ATTRIBUTE_LENGTH);
} else {
// Parse credentials
var credentials = data.toString('utf-8');
var _credentials$split = credentials.split(' '),
ssid = _credentials$split[0],
pwd = _credentials$split[1]; // Input vaidations
if (!ssid) {
logger.error('network ssid is empty', {
ssid: ssid
});
cb(_this.RESULT_UNLIKELY_ERROR);
return;
} // Configue network
_this.network.configureNetwork(ssid, pwd).then(function () {
logger.info('network successfully configured');
cb(_this.RESULT_SUCCESS);
})["catch"](function (err) {
logger.error('unable to configure network', {
err: err
});
cb(_this.RESULT_UNLIKELY_ERROR);
});
}
}
});
}
utils.inherits(NetworkCredentialCharacteristic, bleno.Characteristic);
/**
* BLE characteristic for Network SSID
* @summary Hold the current SSID of the WIFI network connected
* @param network - network
*/
function NetworkSsidCharacteristic(network) {
var _this = this;
this.network = network;
bleno__default.Characteristic.call(this, {
uuid: 'fff3',
properties: ['read'],
descriptors: [new bleno__default.Descriptor({
uuid: '2901',
value: ''
})],
onReadRequest: function onReadRequest(_offset, cb) {
logger.info('bluetooth read network SSID characateristic');
_this.network.getConnectedSSID().then(function (ssid) {
return cb(_this.RESULT_SUCCESS, Buffer.from(ssid));
})["catch"](function (err) {
logger.error('unable to get connected SSID', {
err: err
});
cb(_this.RESULT_UNLIKELY_ERROR);
});
}
});
}
utils.inherits(NetworkSsidCharacteristic, bleno.Characteristic);
/**
* BLE service for Network
* @param network - network
*/
function NetworkService(network) {
bleno__default.PrimaryService.call(this, {
uuid: 'fff1',
characteristics: [// @ts-ignore
new NetworkStatusCharacteristic(network), // @ts-ignore
new NetworkCredentialCharacteristic(network), // @ts-ignore
new NetworkSsidCharacteristic(network)]
});
}
utils.inherits(NetworkService, bleno.PrimaryService);
// A type of promise-like that resolves synchronously and supports only one observer
const _iteratorSymbol = /*#__PURE__*/ typeof Symbol !== "undefined" ? (Symbol.iterator || (Symbol.iterator = Symbol("Symbol.iterator"))) : "@@iterator";
const _asyncIteratorSymbol = /*#__PURE__*/ typeof Symbol !== "undefined" ? (Symbol.asyncIterator || (Symbol.asyncIterator = Symbol("Symbol.asyncIterator"))) : "@@asyncIterator";
// Asynchronously call a function and send errors to recovery continuation
function _catch(body, recover) {
try {
var result = body();
} catch(e) {
return recover(e);
}
if (result && result.then) {
return result.then(void 0, recover);
}
return result;
}
var getConnectedSSID = function getConnectedSSID(ifname) {
try {
logger.info('get connected wifi network SSID', {
ifname: ifname
});
return Promise.resolve(executeCmd.apply(void 0, ['iwgetid', '-r', ifname]));
} catch (e) {
return Promise.reject(e);
}
};
var enableNetwork = function enableNetwork(ifname, networkId) {
try {
logger.info('enable wpa_cli network', {
ifname: ifname,
networkId: networkId
});
return Promise.resolve(executeWpa(ifname, 'enable_network', networkId));
} catch (e) {
return Promise.reject(e);
}
};
var removeNetwork = function removeNetwork(ifname, networkId) {
try {
logger.info('remove wpa_cli network', {
ifname: ifname
});
return Promise.resolve(executeWpa(ifname, 'remove_network', networkId));
} catch (e) {
return Promise.reject(e);
}
};
var addNetwork = function addNetwork(ifname) {
try {
logger.info('create new wpa_cli network', {
ifname: ifname
});
return Promise.resolve(executeWpa(ifname, 'add_network')).then(function (result) {
return parseInt(result, 10);
});
} catch (e) {
return Promise.reject(e);
}
};
var setNetworkVariable = function setNetworkVariable(ifname, networkId, name, value) {
try {
logger.info('set wpa_cli network variable', {
networkId: networkId,
name: name
});
return Promise.resolve(executeWpa(ifname, 'set_network', networkId, name, value)).then(function () {});
} catch (e) {
return Promise.reject(e);
}
};
var findNetworkBySSID = function findNetworkBySSID(ifname, ssid) {
try {
logger.info('find wpa_cli network by SSID', {
ifname: ifname,
ssid: ssid
});
return Promise.resolve(listNetworks(ifname)).then(function (networks) {
// @ts-ignore
return networks.find(function (n) {
return n.ssid === ssid;
});
});
} catch (e) {
return Promise.reject(e);
}
};
var listNetworks = function listNetworks(ifname) {
try {
logger.info('execute wpa_cli list networks', {
ifname: ifname
});
return Promise.resolve(executeWpa(ifname, 'list_networks')).then(function (networksRaw) {
var networkLines = networksRaw.split('\n');
return networkLines.map(function (line) {
var fields = line.split('\t');
var network = {
networkId: parseInt(fields[0], 10),
ssid: fields[1],
bssid: fields[2],
flags: fields[3]
};
return network;
});
});
} catch (e) {
return Promise.reject(e);
}
};
function executeCmd() {
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
var cmd = args.join(' ');
logger.info('execute shell command', {
cmd: cmd
});
return new Promise(function (res, rej) {
child_process.exec(cmd, function (error, stdout, stderr) {
if (error) {
logger.error('shell command execution failed', {
error: error
});
rej(error);
return;
}
if (stderr) {
logger.error('shell command execution error', {
stderr: stderr
});
rej(error);
return;
}
res(stdout.trim());
});
});
}
function executeWpa(ifname) {
for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
args[_key2 - 1] = arguments[_key2];
}
logger.info('execute wpa_cli command', {
ifname: ifname,
args: args
});
return executeCmd.apply(void 0, ['wpa_cli', '-i', ifname].concat(args));
}
var NetworkStatus;
(function (NetworkStatus) {
NetworkStatus["connected"] = "connected";
NetworkStatus["disconnected"] = "disconnected";
})(NetworkStatus || (NetworkStatus = {}));
/**
* Represent network
*/
var Network = /*#__PURE__*/function (_EventEmitter) {
_inheritsLoose(Network, _EventEmitter);
function Network(ifName) {
var _this;
_this = _EventEmitter.call(this) || this;
_this.ssid = '';
_this.ifName = ifName; // Watch for status changing
setInterval(function () {
logger.debug('watch for status change');
var hasChange = _this.hasStatusChanged(_this.status);
if (hasChange) {
_this.emit('statusChange', _this.status, _this.ssid);
}
}, 1000);
return _this;
}
/**
* Get current connected SSID
*/
var _proto = Network.prototype;
_proto.getConnectedSSID = function getConnectedSSID$1() {
try {
var _this3 = this;
return Promise.resolve(getConnectedSSID(_this3.ifName)).then(function (_getCurrentSSID) {
_this3.ssid = _getCurrentSSID;
return _this3.ssid;
});
} catch (e) {
return Promise.reject(e);
}
}
/**
* Get current network connection status
*/
;
_proto.getStatus = function getStatus() {
try {
var _this5 = this;
logger.info('get WIFI network connection status');
return Promise.resolve(_this5.getConnectedSSID()).then(function (ssid) {
_this5.status = ssid ? NetworkStatus.connected : NetworkStatus.disconnected;
return _this5.status;
});
} catch (e) {
return Promise.reject(e);
}
}
/**
* Compare old status with new status
* @param oldStatus - old status
*/
;
_proto.hasStatusChanged = function hasStatusChanged(oldStatus) {
try {
var _this7 = this;
logger.info('check if network connection status has changed');
return Promise.resolve(_this7.getStatus()).then(function (newStatus) {
return newStatus !== oldStatus;
});
} catch (e) {
return Promise.reject(e);
}
}
/**
* Configure WIFI network
* @param ssid - ssid
* @param pwd - password
*/
;
_proto.configureNetwork = function configureNetwork(ssid, pwd) {
try {
var _this9 = this;
logger.info('configure WIFI network', {
ssid: ssid
});
var networkId;
var _temp5 = _catch(function () {
return Promise.resolve(findNetworkBySSID(_this9.ifName, ssid)).then(function (network) {
function _temp2() {
// Set credentials
return Promise.resolve(setNetworkVariable(_this9.ifName, networkId, 'ssid', "'\"" + ssid + "\"'")).then(function () {
return Promise.resolve(setNetworkVariable(_this9.ifName, networkId, 'psk', "'\"" + pwd + "\"'")).then(function () {
return Promise.resolve(enableNetwork(_this9.ifName, networkId)).then(function () {});
});
});
}
var _temp = function () {
if (network.networkId) {
networkId = network.networkId;
} else {
return Promise.resolve(addNetwork(_this9.ifName)).then(function (_addNetwork) {
networkId = _addNetwork;
});
}
}();
// Create or Update network
return _temp && _temp.then ? _temp.then(_temp2) : _temp2(_temp);
});
}, function () {
var _temp3 = function () {
if (networkId) {
return Promise.resolve(removeNetwork(_this9.ifName, networkId)).then(function () {});
}
}();
return _temp3 && _temp3.then ? _temp3.then(function () {}) : void 0;
});
return Promise.resolve(_temp5 && _temp5.then ? _temp5.then(function () {}) : void 0);
} catch (e) {
return Promise.reject(e);
}
};
return Network;
}(events.EventEmitter);
/**
* Status change event
*/
var StatusChangeEvent;
(function (StatusChangeEvent) {
StatusChangeEvent["POWERED_ON"] = "poweredOn";
StatusChangeEvent["POWERED_OFF"] = "poweredOff";
})(StatusChangeEvent || (StatusChangeEvent = {}));
/**
* Main network configuration class
*/
var NetworkConfigurator = /*#__PURE__*/function (_EventEmitter) {
_inheritsLoose(NetworkConfigurator, _EventEmitter);
/**
* Construct instance
* @constructor
* @param config - network configuration
*/
function NetworkConfigurator(config) {
var _config$customService;
var _this;
_this = _EventEmitter.call(this) || this;
_this.deviceName = config.deviceName;
_this.network = new Network(config.ifName);
logger.info('configure network configurator', {
deviceName: config.deviceName,
ifName: config.ifName
}); // Build services
var customServices = (_config$customService = config.customServices) !== null && _config$customService !== void 0 ? _config$customService : []; // @ts-ignore
var networkService = new NetworkService(_this.network);
_this.services = [networkService].concat(customServices); // Bleno events
bleno__default.on('stateChange', _this.stateChange.bind(_assertThisInitialized(_this)));
bleno__default.on('advertisingStart', _this.advertisingStart.bind(_assertThisInitialized(_this))); // Bubble up status change
_this.network.on('statusChange', function (status, ssid) {
return _this.emit('statusChange', status, ssid);
});
return _this;
}
/**
* State change event handler
* @param event - status change event
*/
var _proto = NetworkConfigurator.prototype;
_proto.stateChange = function stateChange(event) {
logger.info('bluetooth services state change', {
event: event
});
switch (event) {
// Powered on
case StatusChangeEvent.POWERED_ON:
var serviceUuids = this.services.map(function (s) {
return s.uuid;
});
bleno__default.startAdvertising(this.deviceName, serviceUuids, function () {
logger.info('bluetooth services advertising started');
});
break;
// Powered off
case StatusChangeEvent.POWERED_OFF:
bleno__default.stopAdvertising(function () {
return logger.warn('bluetooth services advertising stopped');
});
break;
default:
logger.debug('bluetooth services triggered unhandled event', {
event: event
});
}
}
/**
* Adverstising start event handler
* @param error - error
*/
;
_proto.advertisingStart = function advertisingStart(error) {
logger.debug('advertising start callback called');
if (error) {
logger.error('bluetooth advertising starting failed', error);
} else {
bleno__default.setServices(this.services, function (error) {
if (error) {
logger.error('bluetooth services registration failed', error);
} else {
logger.info('bluetooth services registered');
}
});
}
};
return NetworkConfigurator;
}(events.EventEmitter);
exports.NetworkConfigurator = NetworkConfigurator;
//# sourceMappingURL=rasp-blefi.cjs.development.js.map