UNPKG

rasp-blefi

Version:

Experimental project that allow headless configuration of WIFI network on Rasperry PI using BLE.

607 lines (517 loc) 17.2 kB
'use strict'; 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