UNPKG

ircbloq-link

Version:

Porvide local hardware function to ircbloq

347 lines (326 loc) 13 kB
const noble = require('@abandonware/noble'); const Session = require('./session'); const getUUID = id => { if (typeof id === 'number') return id.toString(16); if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(id)) { return id.split('-').join(''); } return id; }; class BLESession extends Session { constructor (socket) { super(socket); this._type = 'ble'; this.peripheral = null; this.services = null; this.characteristics = {}; this.notifyCharacteristics = {}; this.scanningTimeId = null; this.reportedPeripherals = {}; this.discoverListener = null; } async didReceiveCall (method, params, completion) { switch (method) { case 'discover': this.discover(params); completion(null, null); break; case 'connect': await this.connect(params); completion(null, null); break; case 'disconnect': await this.disconnect(params); completion(null, null); break; case 'write': completion(await this.write(params), null); this.repairNotifyAfterWrite(); break; case 'read': completion(await this.read(params), null); break; case 'startNotifications': await this.startNotifications(params); completion(null, null); break; case 'stopNotifications': await this.stopNotifications(params); completion(null, null); break; case 'getServices': completion((this.services || []).map(service => service.uuid), null); break; case 'pingMe': completion('willPing', null); this.sendRemoteRequest('ping', null, result => { console.log(`Got result from ping: ${result}`); }); break; default: throw new Error(`Method not found`); } } discover (params) { if (this.services) { throw new Error('cannot discover when connected'); } const {filters} = params; // if (!Array.isArray(filters) || filters.length < 1) { // throw new Error('discovery request must include filters'); // } // filters.forEach(item => { // const {services} = item; // if (!Array.isArray(services) || services.length < 1) { // throw new Error(`filter contains empty or invalid services list: ${item}`); // } // }); if (this.scanningTimeId) { clearTimeout(this.scanningTimeId); } this.reportedPeripherals = {}; noble.startScanning([], true); this.discoverListener = peripheral => { this.onAdvertisementReceived(peripheral, filters); }; noble.on('discover', this.discoverListener); } onAdvertisementReceived (peripheral, filters) { const {advertisement} = peripheral; if (advertisement) { const finded = (filters || []).find(filter => { const {name, namePrefix, services, manufacturerData} = filter; if (name && name !== advertisement.localName) return false; if (namePrefix && advertisement.localName.indexOf(namePrefix) !== 0) return false; if (services && !services.every(service => { if (!advertisement.serviceUuids) return false; return advertisement.serviceUuids.indexOf(getUUID(service)) !== -1; })) return false; if (manufacturerData && advertisement.manufacturerData) { if (manufacturerData.length !== advertisement.manufacturerData.length) { return false; } if (!manufacturerData.every((data, i) => advertisement.manufacturerData[i] === data)) { return false; } } return !!advertisement.localName; }); if (finded) { this.reportedPeripherals[peripheral.id] = peripheral; this.sendRemoteRequest('didDiscoverPeripheral', { peripheralId: peripheral.id, name: advertisement.localName, rssi: peripheral.rssi }); if (!this.scanningTimeId) { this.scanningTimeId = setTimeout(() => { this.scanningTimeId = null; noble.stopScanning(); if (this.discoverListener) { noble.removeListener('discover', this.discoverListener); } }, 1000); } } } } connect (params) { return new Promise((resolve, reject) => { if (this.peripheral && this.peripheral.state === 'connected') { return reject(new Error('already connected to peripheral')); } const {peripheralId} = params; const peripheral = this.reportedPeripherals[peripheralId]; if (!peripheral) { return reject(new Error(`invalid peripheral ID: ${peripheralId}`)); } if (this.scanningTimeId) { clearTimeout(this.scanningTimeId); this.scanningTimeId = null; noble.stopScanning(); } try { peripheral.connect(error => { if (error) { return reject(new Error(error)); } peripheral.discoverAllServicesAndCharacteristics((err, services) => { if (err) { return reject(new Error(error)); } this.services = services; this.peripheral = peripheral; resolve(); }); }); peripheral.on('disconnect', () => { this.disconnect(); }); } catch (err) { reject(err); } }); } bleWriteData (characteristic, withResponse, data) { return new Promise((resolve, reject) => { characteristic.write(data, !withResponse, err => { if (err) return reject(err); resolve(); }); }); } async write (params) { try { const {message, encoding, withResponse} = params; const buffer = Buffer.from(message, encoding); const characteristic = await this.getEndpoint('write request', params, 'write'); for (let i = 0; i < buffer.length; i += 20) { await this.bleWriteData(characteristic, withResponse, buffer.slice(i, 20)); } return buffer.length; } catch (err) { return new Error(`Error while attempting to write: ${err.message}`); } } bleReadData (characteristic, encoding = 'base64') { return new Promise((resolve, reject) => { characteristic.read((err, data) => { if (err) { return reject(err); } resolve(data.toString(encoding)); }); }); } async read (params) { try { const characteristic = await this.getEndpoint('read request', params, 'read'); const readedData = await this.bleReadData(characteristic); const {startNotifications} = params; if (startNotifications) { await this.startNotifications(params, characteristic); } return readedData; } catch (err) { console.log('Error while attempting to read: ', err); return new Error(`Error while attempting to read: ${err.message}`); } } async startNotifications (params, characteristic) { if (!characteristic || characteristic.properties.indexOf('notify') === -1) { characteristic = await this.getEndpoint('startNotifications request', params, 'notify'); } const uuid = getUUID(characteristic.uuid); if (!this.notifyCharacteristics[uuid]) { this.notifyCharacteristics[uuid] = characteristic; characteristic.subscribe(); } if (!characteristic._events || !characteristic._events.data) { characteristic.on('data', data => { this.onValueChanged(characteristic, data); }); } } async stopNotifications (params) { console.log('stopNotifications !!!'); const characteristic = await this.getEndpoint('stopNotifications request', params, 'notify'); characteristic.unsubscribe(); characteristic.removeAllListeners('data'); delete this.notifyCharacteristics[getUUID(characteristic.uuid)]; } notify (characteristic, notify) { return new Promise((resolve, reject) => { characteristic.notify(notify, err => { if (err) return reject(err); resolve(); }); }); } // noble bug: After write, characteristic object will change repairNotifyAfterWrite () { for (const id in this.notifyCharacteristics) { const characteristic = this.notifyCharacteristics[id]; const {_peripheralId, _serviceUuid, uuid} = characteristic; const currentCharacteristic = noble._characteristics[_peripheralId][_serviceUuid][uuid]; if (characteristic !== currentCharacteristic) { currentCharacteristic._events = characteristic._events; this.notifyCharacteristics[id] = currentCharacteristic; } } } async stopAllNotifications () { for (const id in this.notifyCharacteristics) { await this.notify(this.notifyCharacteristics[id], false); this.notifyCharacteristics[id].removeAllListeners('data'); } } onValueChanged (characteristic, data) { const params = { serviceId: characteristic._serviceUuid, characteristicId: characteristic.uuid, encoding: 'base64', message: data.toString('base64') }; this.sendRemoteRequest('characteristicDidChange', params); } getEndpoint (errorText, params, type) { return new Promise((resolve, reject) => { if (!this.peripheral || this.peripheral.state !== 'connected') { return reject(`Peripheral is not connected for ${errorText}`); } let service; let serviceUuid; let {serviceId, characteristicId} = params; characteristicId = getUUID(characteristicId); if (this.characteristics[characteristicId]) { return resolve(this.characteristics[characteristicId]); } if (serviceId) { serviceId = getUUID(serviceId); service = this.services.find(item => item.uuid === serviceId); } else { service = this.services[0]; serviceUuid = service.uuid; } if (!service) { reject(`Could not determine service UUID for ${errorText}`); } service.discoverCharacteristics([characteristicId], (err, characteristics) => { if (err) { console.warn(err); return reject(`could not find characteristic ${characteristicId} on service ${serviceUuid}`); } const characteristic = characteristics.find(item => item.properties.includes(type)); if (characteristic) { this.characteristics[characteristicId] = characteristic; resolve(characteristic); } else { reject(`failed to collect ${type} characteristic from service`); } }); }); } disconnect () { if (this.peripheral && this.peripheral.state === 'connected') { this.peripheral.disconnect(); } } dispose () { this.disconnect(); super.dispose(); this.stopAllNotifications(); this.socket = null; this.peripheral = null; this.services = null; this.characteristics = null; this.scanningTimeId = null; this.reportedPeripherals = null; this.notifyCharacteristics = null; if (this.discoverListener) { noble.removeListener('discover', this.discoverListener); this.discoverListener = null; } } } module.exports = BLESession;