2020-01-18 15:50:46 +00:00
const dbus = require ( 'dbus-next' ) ;
2020-01-17 14:50:58 +00:00
const crypto = require ( 'crypto' ) ;
const xor = require ( 'buffer-xor' ) ;
const EventEmitter = require ( 'events' ) ;
2021-01-21 21:31:37 +01:00
const Logger = require ( './Logger' ) ;
2020-01-17 14:50:58 +00:00
2021-01-22 15:49:02 +01:00
const logger = Logger . getLogger ( 'plejd-ble' ) ;
2020-01-17 14:50:58 +00:00
// UUIDs
2020-01-19 20:08:48 +00:00
const PLEJD _SERVICE = '31ba0001-6085-4726-be45-040c957391b5' ;
const DATA _UUID = '31ba0004-6085-4726-be45-040c957391b5' ;
const LAST _DATA _UUID = '31ba0005-6085-4726-be45-040c957391b5' ;
const AUTH _UUID = '31ba0009-6085-4726-be45-040c957391b5' ;
const PING _UUID = '31ba000a-6085-4726-be45-040c957391b5' ;
2020-01-17 14:50:58 +00:00
const BLE _CMD _DIM _CHANGE = '00c8' ;
const BLE _CMD _DIM2 _CHANGE = '0098' ;
const BLE _CMD _STATE _CHANGE = '0097' ;
const BLE _CMD _SCENE _TRIG = '0021' ;
const BLUEZ _SERVICE _NAME = 'org.bluez' ;
2020-01-18 15:50:46 +00:00
const DBUS _OM _INTERFACE = 'org.freedesktop.DBus.ObjectManager' ;
const DBUS _PROP _INTERFACE = 'org.freedesktop.DBus.Properties' ;
2020-01-17 14:50:58 +00:00
const BLUEZ _ADAPTER _ID = 'org.bluez.Adapter1' ;
const BLUEZ _DEVICE _ID = 'org.bluez.Device1' ;
const GATT _SERVICE _ID = 'org.bluez.GattService1' ;
const GATT _CHRC _ID = 'org.bluez.GattCharacteristic1' ;
2021-01-01 20:15:00 +01:00
const MAX _TRANSITION _STEPS _PER _SECOND = 5 ; // Could be made a setting
2021-01-18 17:21:37 +01:00
const MAX _RETRY _COUNT = 5 ; // Could be made a setting
2021-01-01 20:15:00 +01:00
2020-01-17 14:50:58 +00:00
class PlejdService extends EventEmitter {
2021-01-22 15:49:02 +01:00
constructor ( cryptoKey , devices , sceneManager , connectionTimeout , writeQueueWaitTime ) {
2020-01-17 14:50:58 +00:00
super ( ) ;
2021-01-21 21:31:37 +01:00
logger . info ( 'Starting Plejd BLE, resetting all device states.' ) ;
2021-01-18 17:21:37 +01:00
2020-01-17 14:50:58 +00:00
this . cryptoKey = Buffer . from ( cryptoKey . replace ( /-/g , '' ) , 'hex' ) ;
2020-02-29 15:54:08 +00:00
this . sceneManager = sceneManager ;
this . connectedDevice = null ;
2020-01-19 20:08:48 +00:00
this . plejdService = null ;
this . bleDevices = [ ] ;
2021-01-01 20:15:00 +01:00
this . bleDeviceTransitionTimers = { } ;
2020-01-17 14:50:58 +00:00
this . plejdDevices = { } ;
2020-02-29 15:54:08 +00:00
this . devices = devices ;
2020-01-17 14:50:58 +00:00
this . connectEventHooked = false ;
2020-01-27 20:43:52 +00:00
this . connectionTimeout = connectionTimeout ;
2020-03-03 16:22:30 +01:00
this . writeQueueWaitTime = writeQueueWaitTime ;
2020-02-29 15:54:08 +00:00
this . writeQueue = [ ] ;
this . writeQueueRef = null ;
2021-01-18 17:21:37 +01:00
this . initInProgress = null ;
2020-01-17 14:50:58 +00:00
// Holds a reference to all characteristics
this . characteristics = {
data : null ,
lastData : null ,
2020-01-19 20:08:48 +00:00
lastDataProperties : null ,
2020-01-17 14:50:58 +00:00
auth : null ,
2021-01-22 15:49:02 +01:00
ping : null ,
2020-01-17 14:50:58 +00:00
} ;
2020-01-18 15:50:46 +00:00
this . bus = dbus . systemBus ( ) ;
2020-01-19 20:08:48 +00:00
this . adapter = null ;
2020-01-17 14:50:58 +00:00
2021-01-21 21:31:37 +01:00
logger . debug ( 'wiring events and waiting for BLE interface to power up.' ) ;
2020-01-17 14:50:58 +00:00
this . wireEvents ( ) ;
}
async init ( ) {
if ( this . objectManager ) {
this . objectManager . removeAllListeners ( ) ;
}
2021-01-13 01:47:05 +01:00
this . bleDevices = [ ] ;
2020-02-29 15:54:08 +00:00
this . connectedDevice = null ;
2021-01-13 01:47:05 +01:00
2020-01-21 14:24:02 +00:00
this . characteristics = {
data : null ,
lastData : null ,
lastDataProperties : null ,
auth : null ,
2021-01-22 15:49:02 +01:00
ping : null ,
2020-01-21 14:24:02 +00:00
} ;
2020-01-27 20:43:52 +00:00
2020-01-20 10:58:03 +00:00
clearInterval ( this . pingRef ) ;
2021-01-13 19:43:11 +01:00
clearTimeout ( this . writeQueueRef ) ;
2021-01-21 21:31:37 +01:00
logger . info ( 'init()' ) ;
2020-01-20 10:58:03 +00:00
2020-01-18 15:50:46 +00:00
const bluez = await this . bus . getProxyObject ( BLUEZ _SERVICE _NAME , '/' ) ;
this . objectManager = await bluez . getInterface ( DBUS _OM _INTERFACE ) ;
2020-01-17 14:50:58 +00:00
2020-01-19 20:08:48 +00:00
// We need to find the ble interface which implements the Adapter1 interface
const managedObjects = await this . objectManager . GetManagedObjects ( ) ;
2021-01-22 15:49:02 +01:00
const result = await this . _getInterface ( managedObjects , BLUEZ _ADAPTER _ID ) ;
2020-01-17 14:50:58 +00:00
2020-01-19 20:08:48 +00:00
if ( result ) {
this . adapter = result [ 1 ] ;
}
2020-01-17 14:50:58 +00:00
2020-01-19 20:08:48 +00:00
if ( ! this . adapter ) {
2021-01-22 15:49:02 +01:00
logger . error ( 'Unable to find a bluetooth adapter that is compatible.' ) ;
return Promise . reject ( new Error ( 'Unable to find a bluetooth adapter that is compatible.' ) ) ;
2020-01-17 14:50:58 +00:00
}
2021-01-22 15:49:02 +01:00
// eslint-disable-next-line no-restricted-syntax
for ( const path of Object . keys ( managedObjects ) ) {
/* eslint-disable no-await-in-loop */
2020-01-19 20:08:48 +00:00
const interfaces = Object . keys ( managedObjects [ path ] ) ;
2020-01-20 07:58:23 +00:00
2020-01-19 20:08:48 +00:00
if ( interfaces . indexOf ( BLUEZ _DEVICE _ID ) > - 1 ) {
const proxyObject = await this . bus . getProxyObject ( BLUEZ _SERVICE _NAME , path ) ;
const device = await proxyObject . getInterface ( BLUEZ _DEVICE _ID ) ;
2020-01-17 14:50:58 +00:00
2020-01-19 20:08:48 +00:00
const connected = managedObjects [ path ] [ BLUEZ _DEVICE _ID ] . Connected . value ;
if ( connected ) {
2021-01-22 15:49:02 +01:00
logger . info ( ` disconnecting ${ path } ` ) ;
2020-01-19 20:08:48 +00:00
await device . Disconnect ( ) ;
}
await this . adapter . RemoveDevice ( path ) ;
}
2021-01-22 15:49:02 +01:00
/* eslint-enable no-await-in-loop */
2020-01-17 14:50:58 +00:00
}
2020-01-19 20:08:48 +00:00
this . objectManager . on ( 'InterfacesAdded' , this . onInterfacesAdded . bind ( this ) ) ;
this . adapter . SetDiscoveryFilter ( {
2021-01-22 15:49:02 +01:00
UUIDs : new dbus . Variant ( 'as' , [ PLEJD _SERVICE ] ) ,
Transport : new dbus . Variant ( 's' , 'le' ) ,
2020-01-19 20:08:48 +00:00
} ) ;
2020-01-27 20:43:52 +00:00
try {
await this . adapter . StartDiscovery ( ) ;
2020-06-12 11:15:00 +02:00
} catch ( err ) {
2021-01-22 15:49:02 +01:00
logger . error ( 'Failed to start discovery. Make sure no other add-on is currently scanning.' ) ;
return Promise . reject (
new Error ( 'Failed to start discovery. Make sure no other add-on is currently scanning.' ) ,
) ;
}
return new Promise ( ( resolve ) => setTimeout (
( ) => resolve (
this . _internalInit ( ) . catch ( ( err ) => {
logger . error ( 'InternalInit exception! Will rethrow.' , err ) ;
throw err ;
} ) ,
) ,
this . connectionTimeout * 1000 ,
) ) ;
2020-01-19 20:08:48 +00:00
}
2020-01-17 14:50:58 +00:00
2020-01-19 20:08:48 +00:00
async _internalInit ( ) {
2021-01-21 21:31:37 +01:00
logger . debug ( ` Got ${ this . bleDevices . length } device(s). ` ) ;
2020-01-17 14:50:58 +00:00
2021-01-22 15:49:02 +01:00
// eslint-disable-next-line no-restricted-syntax
2020-01-19 20:08:48 +00:00
for ( const plejd of this . bleDevices ) {
2021-01-22 15:49:02 +01:00
/* eslint-disable no-await-in-loop */
logger . debug ( ` Inspecting ${ plejd . path } ` ) ;
2020-01-17 14:50:58 +00:00
2020-01-19 20:08:48 +00:00
try {
2021-01-22 15:49:02 +01:00
const proxyObject = await this . bus . getProxyObject ( BLUEZ _SERVICE _NAME , plejd . path ) ;
2020-01-19 20:08:48 +00:00
const device = await proxyObject . getInterface ( BLUEZ _DEVICE _ID ) ;
const properties = await proxyObject . getInterface ( DBUS _PROP _INTERFACE ) ;
2020-01-20 07:58:23 +00:00
2021-01-22 15:49:02 +01:00
plejd . rssi = ( await properties . Get ( BLUEZ _DEVICE _ID , 'RSSI' ) ) . value ;
plejd . instance = device ;
2020-01-17 14:50:58 +00:00
2021-01-22 15:49:02 +01:00
const segments = plejd . path . split ( '/' ) ;
2020-02-29 15:54:08 +00:00
let fixedPlejdPath = segments [ segments . length - 1 ] . replace ( 'dev_' , '' ) ;
fixedPlejdPath = fixedPlejdPath . replace ( /_/g , '' ) ;
2021-01-22 15:49:02 +01:00
plejd . device = this . devices . find ( ( x ) => x . serialNumber === fixedPlejdPath ) ;
2020-02-29 15:54:08 +00:00
2021-01-22 15:49:02 +01:00
logger . debug ( ` Discovered ${ plejd . path } with rssi ${ plejd . rssi } ` ) ;
2020-06-12 11:15:00 +02:00
} catch ( err ) {
2021-01-22 15:49:02 +01:00
logger . error ( ` Failed inspecting ${ plejd . path } . ` , err ) ;
2020-01-17 14:50:58 +00:00
}
2021-01-22 15:49:02 +01:00
/* eslint-enable no-await-in-loop */
2020-01-19 20:08:48 +00:00
}
2021-01-22 15:49:02 +01:00
const sortedDevices = this . bleDevices . sort ( ( a , b ) => b . rssi - a . rssi ) ;
2020-01-19 20:08:48 +00:00
let connectedDevice = null ;
2020-01-17 14:50:58 +00:00
2021-01-22 15:49:02 +01:00
// eslint-disable-next-line no-restricted-syntax
2020-01-19 20:08:48 +00:00
for ( const plejd of sortedDevices ) {
try {
2021-01-22 15:49:02 +01:00
if ( plejd . instance ) {
logger . info ( ` Connecting to ${ plejd . path } ` ) ;
// eslint-disable-next-line no-await-in-loop
await plejd . instance . Connect ( ) ;
2020-01-27 20:43:52 +00:00
connectedDevice = plejd ;
2021-01-22 15:49:02 +01:00
break ;
2020-01-27 20:43:52 +00:00
}
2020-06-12 11:15:00 +02:00
} catch ( err ) {
2021-01-21 21:31:37 +01:00
logger . error ( 'Warning: unable to connect, will retry. ' , err ) ;
2020-01-19 20:08:48 +00:00
}
2020-01-17 14:50:58 +00:00
}
2020-01-19 20:08:48 +00:00
setTimeout ( async ( ) => {
await this . onDeviceConnected ( connectedDevice ) ;
await this . adapter . StopDiscovery ( ) ;
2020-01-27 20:43:52 +00:00
} , this . connectionTimeout * 1000 ) ;
2020-01-19 20:08:48 +00:00
}
2020-01-17 14:50:58 +00:00
2020-01-19 20:08:48 +00:00
async _getInterface ( managedObjects , iface ) {
const managedPaths = Object . keys ( managedObjects ) ;
2021-01-22 15:49:02 +01:00
// eslint-disable-next-line no-restricted-syntax
for ( const path of managedPaths ) {
2020-01-19 20:08:48 +00:00
const pathInterfaces = Object . keys ( managedObjects [ path ] ) ;
if ( pathInterfaces . indexOf ( iface ) > - 1 ) {
2021-01-21 21:31:37 +01:00
logger . debug ( ` Found BLE interface ' ${ iface } ' at ${ path } ` ) ;
2020-01-19 20:08:48 +00:00
try {
2021-01-22 15:49:02 +01:00
// eslint-disable-next-line no-await-in-loop
2020-01-19 20:08:48 +00:00
const adapterObject = await this . bus . getProxyObject ( BLUEZ _SERVICE _NAME , path ) ;
return [ path , adapterObject . getInterface ( iface ) , adapterObject ] ;
2020-06-12 11:15:00 +02:00
} catch ( err ) {
2021-01-21 21:31:37 +01:00
logger . error ( ` Failed to get interface ' ${ iface } '. ` , err ) ;
2020-01-19 20:08:48 +00:00
}
2020-01-17 14:50:58 +00:00
}
2020-01-19 20:08:48 +00:00
}
return null ;
}
2020-01-17 14:50:58 +00:00
2020-01-19 20:08:48 +00:00
async onInterfacesAdded ( path , interfaces ) {
2021-01-01 20:15:00 +01:00
// const [adapter, dev, service, characteristic] = path.split('/').slice(3);
2020-01-19 20:08:48 +00:00
const interfaceKeys = Object . keys ( interfaces ) ;
if ( interfaceKeys . indexOf ( BLUEZ _DEVICE _ID ) > - 1 ) {
2021-01-22 15:49:02 +01:00
if ( interfaces [ BLUEZ _DEVICE _ID ] . UUIDs . value . indexOf ( PLEJD _SERVICE ) > - 1 ) {
2021-01-21 21:31:37 +01:00
logger . debug ( ` Found Plejd service on ${ path } ` ) ;
2020-06-12 11:15:00 +02:00
this . bleDevices . push ( {
2021-01-22 15:49:02 +01:00
path ,
2020-06-12 11:15:00 +02:00
} ) ;
2020-01-19 20:08:48 +00:00
} else {
2021-01-21 21:31:37 +01:00
logger . error ( 'Uh oh, no Plejd device!' ) ;
2020-01-17 14:50:58 +00:00
}
}
}
2021-01-18 17:21:37 +01:00
turnOn ( deviceId , command ) {
2021-01-21 21:31:37 +01:00
const deviceName = this . _getDeviceName ( deviceId ) ;
2021-01-22 15:49:02 +01:00
logger . info (
` Plejd got turn on command for ${ deviceName } ( ${ deviceId } ), brightness ${ command . brightness } ${
command . transition ? ` , transition: ${ command . transition } ` : ''
} ` ,
) ;
2021-01-18 17:21:37 +01:00
this . _transitionTo ( deviceId , command . brightness , command . transition , deviceName ) ;
2021-01-01 20:15:00 +01:00
}
2020-01-17 14:50:58 +00:00
2021-01-18 17:21:37 +01:00
turnOff ( deviceId , command ) {
2021-01-21 21:31:37 +01:00
const deviceName = this . _getDeviceName ( deviceId ) ;
2021-01-22 15:49:02 +01:00
logger . info (
` Plejd got turn off command for ${ deviceName } ( ${ deviceId } ), brightness ${
command . brightness
} $ { command . transition ? ` , transition: ${ command . transition } ` : '' } ` ,
) ;
2021-01-18 17:21:37 +01:00
this . _transitionTo ( deviceId , 0 , command . transition , deviceName ) ;
2021-01-01 20:15:00 +01:00
}
2020-01-17 14:50:58 +00:00
2021-01-18 17:21:37 +01:00
_clearDeviceTransitionTimer ( deviceId ) {
if ( this . bleDeviceTransitionTimers [ deviceId ] ) {
clearInterval ( this . bleDeviceTransitionTimers [ deviceId ] ) ;
2020-01-17 14:50:58 +00:00
}
}
2021-01-18 17:21:37 +01:00
_transitionTo ( deviceId , targetBrightness , transition , deviceName ) {
2021-01-22 15:49:02 +01:00
const initialBrightness = this . plejdDevices [ deviceId ]
? this . plejdDevices [ deviceId ] . state && this . plejdDevices [ deviceId ] . dim
: null ;
2021-01-02 10:04:24 +01:00
this . _clearDeviceTransitionTimer ( deviceId ) ;
2020-01-17 14:50:58 +00:00
2021-01-22 15:49:02 +01:00
const isDimmable = this . devices . find ( ( d ) => d . id === deviceId ) . dimmable ;
2020-01-17 14:50:58 +00:00
2021-01-22 15:49:02 +01:00
if (
transition > 1
&& isDimmable
&& ( initialBrightness || initialBrightness === 0 )
&& ( targetBrightness || targetBrightness === 0 )
&& targetBrightness !== initialBrightness
) {
2021-01-01 20:15:00 +01:00
// Transition time set, known initial and target brightness
// Calculate transition interval time based on delta brightness and max steps per second
// During transition, measure actual transition interval time and adjust stepping continously
2021-01-22 15:49:02 +01:00
// If transition <= 1 second, Plejd will do a better job
// than we can in transitioning so transitioning will be skipped
2021-01-01 20:15:00 +01:00
const deltaBrightness = targetBrightness - initialBrightness ;
2021-01-22 15:49:02 +01:00
const transitionSteps = Math . min (
Math . abs ( deltaBrightness ) ,
MAX _TRANSITION _STEPS _PER _SECOND * transition ,
) ;
const transitionInterval = ( transition * 1000 ) / transitionSteps ;
logger . debug (
` transitioning from ${ initialBrightness } to ${ targetBrightness } ${
transition ? ` in ${ transition } seconds ` : ''
} . ` ,
) ;
logger . verbose (
` delta brightness ${ deltaBrightness } , steps ${ transitionSteps } , interval ${ transitionInterval } ms ` ,
) ;
2021-01-13 01:47:05 +01:00
2021-01-01 20:15:00 +01:00
const dtStart = new Date ( ) ;
let nSteps = 0 ;
2021-01-02 10:04:24 +01:00
this . bleDeviceTransitionTimers [ deviceId ] = setInterval ( ( ) => {
2021-01-22 15:49:02 +01:00
const tElapsedMs = new Date ( ) . getTime ( ) - dtStart . getTime ( ) ;
2021-01-01 20:15:00 +01:00
let tElapsed = tElapsedMs / 1000 ;
2021-01-13 01:47:05 +01:00
2021-01-01 20:15:00 +01:00
if ( tElapsed > transition || tElapsed < 0 ) {
tElapsed = transition ;
2020-01-17 14:50:58 +00:00
}
2021-01-22 15:49:02 +01:00
let newBrightness = Math . round (
initialBrightness + ( deltaBrightness * tElapsed ) / transition ,
) ;
2020-01-17 14:50:58 +00:00
2021-01-01 20:15:00 +01:00
if ( tElapsed === transition ) {
nSteps ++ ;
2021-01-02 10:04:24 +01:00
this . _clearDeviceTransitionTimer ( deviceId ) ;
2021-01-01 20:15:00 +01:00
newBrightness = targetBrightness ;
2021-01-22 15:49:02 +01:00
logger . debug (
` Queueing finalize ${ deviceName } ( ${ deviceId } ) transition from ${ initialBrightness } to ${ targetBrightness } in ${ tElapsedMs } ms. Done steps ${ nSteps } . Average interval ${
tElapsedMs / ( nSteps || 1 )
} ms . ` ,
) ;
2021-01-18 17:21:37 +01:00
this . _setBrightness ( deviceId , newBrightness , true , deviceName ) ;
} else {
2021-01-01 20:15:00 +01:00
nSteps ++ ;
2021-01-22 15:49:02 +01:00
logger . verbose (
` Queueing dim transition for ${ deviceName } ( ${ deviceId } ) to ${ newBrightness } . Total queue length ${ this . writeQueue . length } ` ,
) ;
2021-01-18 17:21:37 +01:00
this . _setBrightness ( deviceId , newBrightness , false , deviceName ) ;
2021-01-01 20:15:00 +01:00
}
} , transitionInterval ) ;
2021-01-22 15:49:02 +01:00
} else {
2021-01-01 20:15:00 +01:00
if ( transition && isDimmable ) {
2021-01-22 15:49:02 +01:00
logger . debug (
` Could not transition light change. Either initial value is unknown or change is too small. Requested from ${ initialBrightness } to ${ targetBrightness } ` ,
) ;
2021-01-01 20:15:00 +01:00
}
2021-01-18 17:21:37 +01:00
this . _setBrightness ( deviceId , targetBrightness , true , deviceName ) ;
2021-01-01 20:15:00 +01:00
}
}
2021-01-18 17:21:37 +01:00
_setBrightness ( deviceId , brightness , shouldRetry , deviceName ) {
let payload = null ;
let log = '' ;
2021-01-02 10:04:24 +01:00
if ( ! brightness && brightness !== 0 ) {
2021-01-22 15:49:02 +01:00
logger . debug (
` Queueing turn on ${ deviceName } ( ${ deviceId } ). No brightness specified, setting DIM to previous. ` ,
) ;
payload = Buffer . from ( ` ${ deviceId . toString ( 16 ) . padStart ( 2 , '0' ) } 0110009701 ` , 'hex' ) ;
2021-01-18 17:21:37 +01:00
log = 'ON' ;
2021-01-22 15:49:02 +01:00
} else if ( brightness <= 0 ) {
logger . debug ( ` Queueing turn off ${ deviceId } ` ) ;
payload = Buffer . from ( ` ${ deviceId . toString ( 16 ) . padStart ( 2 , '0' ) } 0110009700 ` , 'hex' ) ;
log = 'OFF' ;
} else {
if ( brightness > 255 ) {
// eslint-disable-next-line no-param-reassign
brightness = 255 ;
2021-01-01 20:15:00 +01:00
}
2021-01-22 15:49:02 +01:00
logger . debug ( ` Queueing ${ deviceId } set brightness to ${ brightness } ` ) ;
// eslint-disable-next-line no-bitwise
const brightnessVal = ( brightness << 8 ) | brightness ;
payload = Buffer . from (
` ${ deviceId . toString ( 16 ) . padStart ( 2 , '0' ) } 0110009801 ${ brightnessVal
. toString ( 16 )
. padStart ( 4 , '0' ) } ` ,
'hex' ,
) ;
log = ` DIM ${ brightness } ` ;
}
this . writeQueue . unshift ( {
deviceId ,
log ,
shouldRetry ,
payload ,
} ) ;
2020-02-29 15:54:08 +00:00
}
triggerScene ( sceneIndex ) {
2021-01-18 17:21:37 +01:00
const sceneName = this . _getDeviceName ( sceneIndex ) ;
2021-01-22 15:49:02 +01:00
logger . info (
` Triggering scene ${ sceneName } ( ${ sceneIndex } ). Scene name might be misleading if there is a device with the same numeric id. ` ,
) ;
2020-02-29 15:54:08 +00:00
this . sceneManager . executeScene ( sceneIndex , this ) ;
2020-01-17 14:50:58 +00:00
}
2020-01-19 20:08:48 +00:00
async authenticate ( ) {
2021-01-21 21:31:37 +01:00
logger . info ( 'authenticate()' ) ;
2020-01-17 14:50:58 +00:00
2020-01-19 20:08:48 +00:00
try {
2021-01-21 21:31:37 +01:00
logger . debug ( 'Sending challenge to device' ) ;
2020-01-19 20:08:48 +00:00
await this . characteristics . auth . WriteValue ( [ 0 ] , { } ) ;
2021-01-21 21:31:37 +01:00
logger . debug ( 'Reading response from device' ) ;
2020-01-19 20:08:48 +00:00
const challenge = await this . characteristics . auth . ReadValue ( { } ) ;
2020-01-20 21:32:46 +00:00
const response = this . _createChallengeResponse ( this . cryptoKey , Buffer . from ( challenge ) ) ;
2021-01-21 21:31:37 +01:00
logger . debug ( 'Responding to authenticate' ) ;
2020-01-19 20:08:48 +00:00
await this . characteristics . auth . WriteValue ( [ ... response ] , { } ) ;
2020-06-12 11:15:00 +02:00
} catch ( err ) {
2021-01-21 21:31:37 +01:00
logger . error ( 'Failed to authenticate: ' , err ) ;
2020-01-19 20:08:48 +00:00
}
2020-01-21 14:24:02 +00:00
// auth done, start ping
2021-01-13 19:43:11 +01:00
this . startPing ( ) ;
this . startWriteQueue ( ) ;
2020-01-21 14:24:02 +00:00
// After we've authenticated, we need to hook up the event listener
// for changes to lastData.
2021-01-22 15:49:02 +01:00
this . characteristics . lastDataProperties . on (
'PropertiesChanged' ,
this . onLastDataUpdated . bind ( this ) ,
) ;
2020-01-21 14:24:02 +00:00
this . characteristics . lastData . StartNotify ( ) ;
2020-01-17 14:50:58 +00:00
}
2021-01-13 01:47:05 +01:00
async throttledInit ( delay ) {
2021-01-22 15:49:02 +01:00
if ( this . initInProgress ) {
logger . debug (
'ThrottledInit already in progress. Skipping this call and returning existing promise.' ,
) ;
2021-01-18 17:21:37 +01:00
return this . initInProgress ;
2021-01-13 01:47:05 +01:00
}
2021-01-22 15:49:02 +01:00
this . initInProgress = new Promise ( ( resolve ) => setTimeout ( async ( ) => {
const result = await this . init ( ) . catch ( ( err ) => {
logger . error ( 'TrottledInit exception calling init(). Will re-throw.' , err ) ;
throw err ;
} ) ;
this . initInProgress = null ;
resolve ( result ) ;
} , delay ) ) ;
return this . initInProgress ;
2021-01-13 01:47:05 +01:00
}
2021-01-18 17:21:37 +01:00
async write ( data ) {
2021-01-13 01:47:05 +01:00
if ( ! data || ! this . plejdService || ! this . characteristics . data ) {
2021-01-21 21:31:37 +01:00
logger . debug ( 'data, plejdService or characteristics not available. Cannot write()' ) ;
2021-01-22 15:49:02 +01:00
return false ;
2020-01-24 10:20:39 +01:00
}
2020-01-19 20:08:48 +00:00
try {
2021-01-21 21:31:37 +01:00
logger . verbose ( ` Sending ${ data . length } byte(s) of data to Plejd ` , data ) ;
2020-01-19 20:08:48 +00:00
const encryptedData = this . _encryptDecrypt ( this . cryptoKey , this . plejdService . addr , data ) ;
await this . characteristics . data . WriteValue ( [ ... encryptedData ] , { } ) ;
2021-01-18 17:21:37 +01:00
return true ;
2020-06-12 11:15:00 +02:00
} catch ( err ) {
2020-03-03 15:59:10 +01:00
if ( err . message === 'In Progress' ) {
2021-01-22 15:49:02 +01:00
logger . debug ( "Write failed due to 'In progress' " , err ) ;
2021-01-18 17:21:37 +01:00
} else {
2021-01-21 21:31:37 +01:00
logger . debug ( 'Write failed ' , err ) ;
2020-03-03 15:59:10 +01:00
}
2021-01-13 01:47:05 +01:00
await this . throttledInit ( this . connectionTimeout * 1000 ) ;
2021-01-18 17:21:37 +01:00
return false ;
2020-01-19 20:08:48 +00:00
}
2020-01-17 14:50:58 +00:00
}
2021-01-13 19:43:11 +01:00
startPing ( ) {
2021-01-21 21:31:37 +01:00
logger . info ( 'startPing()' ) ;
2020-01-17 14:50:58 +00:00
clearInterval ( this . pingRef ) ;
this . pingRef = setInterval ( async ( ) => {
2021-01-21 21:31:37 +01:00
logger . silly ( 'ping' ) ;
2020-01-19 20:08:48 +00:00
await this . ping ( ) ;
2020-01-20 21:32:46 +00:00
} , 3000 ) ;
2020-01-17 14:50:58 +00:00
}
2021-01-22 15:49:02 +01:00
// eslint-disable-next-line class-methods-use-this
2020-01-17 14:50:58 +00:00
onPingSuccess ( nr ) {
2021-01-22 15:49:02 +01:00
logger . silly ( ` pong: ${ nr } ` ) ;
2020-01-17 14:50:58 +00:00
}
2020-01-19 20:08:48 +00:00
async onPingFailed ( error ) {
2021-01-22 15:49:02 +01:00
logger . debug ( ` onPingFailed( ${ error } ) ` ) ;
2021-01-21 21:31:37 +01:00
logger . info ( 'ping failed, reconnecting.' ) ;
2020-01-17 14:50:58 +00:00
2020-01-19 20:08:48 +00:00
clearInterval ( this . pingRef ) ;
2021-01-22 15:49:02 +01:00
return this . init ( ) . catch ( ( err ) => {
logger . error ( 'onPingFailed exception calling init(). Will swallow error.' , err ) ;
} ) ;
2020-01-17 14:50:58 +00:00
}
2020-01-19 20:08:48 +00:00
async ping ( ) {
2021-01-21 21:31:37 +01:00
logger . silly ( 'ping()' ) ;
2020-01-17 14:50:58 +00:00
2021-01-22 15:49:02 +01:00
const ping = crypto . randomBytes ( 1 ) ;
2020-01-19 20:08:48 +00:00
let pong = null ;
try {
await this . characteristics . ping . WriteValue ( [ ... ping ] , { } ) ;
pong = await this . characteristics . ping . ReadValue ( { } ) ;
2020-06-12 11:15:00 +02:00
} catch ( err ) {
2021-01-21 21:31:37 +01:00
logger . error ( 'Error writing to plejd: ' , err ) ;
2020-01-19 20:08:48 +00:00
this . emit ( 'pingFailed' , 'write error' ) ;
2020-01-17 14:50:58 +00:00
return ;
}
2021-01-22 15:49:02 +01:00
// eslint-disable-next-line no-bitwise
2020-01-19 20:08:48 +00:00
if ( ( ( ping [ 0 ] + 1 ) & 0xff ) !== pong [ 0 ] ) {
2021-01-21 21:31:37 +01:00
logger . error ( 'Plejd ping failed' ) ;
2021-01-22 15:49:02 +01:00
this . emit ( 'pingFailed' , ` plejd ping failed ${ ping [ 0 ] } - ${ pong [ 0 ] } ` ) ;
2020-01-19 20:08:48 +00:00
return ;
}
2020-01-17 14:50:58 +00:00
2020-01-19 20:08:48 +00:00
this . emit ( 'pingSuccess' , pong [ 0 ] ) ;
2020-01-17 14:50:58 +00:00
}
2021-01-13 19:43:11 +01:00
startWriteQueue ( ) {
2021-01-21 21:31:37 +01:00
logger . info ( 'startWriteQueue()' ) ;
2021-01-13 19:43:11 +01:00
clearTimeout ( this . writeQueueRef ) ;
2020-02-29 15:54:08 +00:00
2020-03-03 16:22:30 +01:00
this . writeQueueRef = setTimeout ( ( ) => this . runWriteQueue ( ) , this . writeQueueWaitTime ) ;
2020-03-03 15:59:10 +01:00
}
async runWriteQueue ( ) {
2021-01-18 17:21:37 +01:00
try {
while ( this . writeQueue . length > 0 ) {
const queueItem = this . writeQueue . pop ( ) ;
const deviceName = this . _getDeviceName ( queueItem . deviceId ) ;
2021-01-22 15:49:02 +01:00
logger . debug (
` Write queue: Processing ${ deviceName } ( ${ queueItem . deviceId } ). Command ${ queueItem . log } . Total queue length: ${ this . writeQueue . length } ` ,
) ;
2021-01-18 17:21:37 +01:00
if ( this . writeQueue . some ( ( item ) => item . deviceId === queueItem . deviceId ) ) {
2021-01-22 15:49:02 +01:00
logger . verbose (
` Skipping ${ deviceName } ( ${ queueItem . deviceId } ) `
+ ` ${ queueItem . log } due to more recent command in queue. ` ,
) ;
// Skip commands if new ones exist for the same deviceId
// still process all messages in order
} else {
// eslint-disable-next-line no-await-in-loop
const success = await this . write ( queueItem . payload ) ;
if ( ! success && queueItem . shouldRetry ) {
queueItem . retryCount = ( queueItem . retryCount || 0 ) + 1 ;
logger . debug ( ` Will retry command, count failed so far ${ queueItem . retryCount } ` ) ;
if ( queueItem . retryCount <= MAX _RETRY _COUNT ) {
this . writeQueue . push ( queueItem ) ; // Add back to top of queue to be processed next;
} else {
logger . error (
` Write queue: Exceeed max retry count ( ${ MAX _RETRY _COUNT } ) for ${ deviceName } ( ${ queueItem . deviceId } ). Command ${ queueItem . log } failed. ` ,
) ;
break ;
}
if ( queueItem . retryCount > 1 ) {
break ; // First retry directly, consecutive after writeQueueWaitTime ms
}
2021-01-18 17:21:37 +01:00
}
}
}
} catch ( e ) {
2021-01-21 21:31:37 +01:00
logger . error ( 'Error in writeQueue loop, values probably not written to Plejd' , e ) ;
2020-03-03 15:59:10 +01:00
}
2020-03-03 16:22:30 +01:00
this . writeQueueRef = setTimeout ( ( ) => this . runWriteQueue ( ) , this . writeQueueWaitTime ) ;
2020-02-29 15:54:08 +00:00
}
2020-01-19 20:08:48 +00:00
async _processPlejdService ( path , characteristics ) {
const proxyObject = await this . bus . getProxyObject ( BLUEZ _SERVICE _NAME , path ) ;
const properties = await proxyObject . getInterface ( DBUS _PROP _INTERFACE ) ;
2020-01-17 14:50:58 +00:00
2020-01-19 20:08:48 +00:00
const uuid = ( await properties . Get ( GATT _SERVICE _ID , 'UUID' ) ) . value ;
if ( uuid !== PLEJD _SERVICE ) {
2021-01-21 21:31:37 +01:00
logger . error ( 'not a Plejd device.' ) ;
2020-01-19 20:08:48 +00:00
return null ;
}
2020-01-17 14:50:58 +00:00
2020-01-19 20:08:48 +00:00
const dev = ( await properties . Get ( GATT _SERVICE _ID , 'Device' ) ) . value ;
const regex = /dev_([0-9A-F_]+)$/ ;
const dirtyAddr = regex . exec ( dev ) ;
const addr = this . _reverseBuffer (
2021-01-22 15:49:02 +01:00
Buffer . from ( String ( dirtyAddr [ 1 ] ) . replace ( /-/g , '' ) . replace ( /_/g , '' ) . replace ( /:/g , '' ) , 'hex' ) ,
2020-01-19 20:08:48 +00:00
) ;
2021-01-22 15:49:02 +01:00
// eslint-disable-next-line no-restricted-syntax
2020-01-19 20:08:48 +00:00
for ( const chPath of characteristics ) {
2021-01-22 15:49:02 +01:00
/* eslint-disable no-await-in-loop */
2020-01-19 20:08:48 +00:00
const chProxyObject = await this . bus . getProxyObject ( BLUEZ _SERVICE _NAME , chPath ) ;
const ch = await chProxyObject . getInterface ( GATT _CHRC _ID ) ;
const prop = await chProxyObject . getInterface ( DBUS _PROP _INTERFACE ) ;
const chUuid = ( await prop . Get ( GATT _CHRC _ID , 'UUID' ) ) . value ;
if ( chUuid === DATA _UUID ) {
2021-01-21 21:31:37 +01:00
logger . debug ( 'found DATA characteristic.' ) ;
2020-01-19 20:08:48 +00:00
this . characteristics . data = ch ;
2020-06-12 11:15:00 +02:00
} else if ( chUuid === LAST _DATA _UUID ) {
2021-01-21 21:31:37 +01:00
logger . debug ( 'found LAST_DATA characteristic.' ) ;
2020-01-19 20:08:48 +00:00
this . characteristics . lastData = ch ;
this . characteristics . lastDataProperties = prop ;
2020-06-12 11:15:00 +02:00
} else if ( chUuid === AUTH _UUID ) {
2021-01-21 21:31:37 +01:00
logger . debug ( 'found AUTH characteristic.' ) ;
2020-01-19 20:08:48 +00:00
this . characteristics . auth = ch ;
2020-06-12 11:15:00 +02:00
} else if ( chUuid === PING _UUID ) {
2021-01-21 21:31:37 +01:00
logger . debug ( 'found PING characteristic.' ) ;
2020-01-19 20:08:48 +00:00
this . characteristics . ping = ch ;
}
2021-01-22 15:49:02 +01:00
/* eslint-eslint no-await-in-loop */
2020-01-19 20:08:48 +00:00
}
return {
2021-01-22 15:49:02 +01:00
addr ,
2020-01-19 20:08:48 +00:00
} ;
2020-01-17 14:50:58 +00:00
}
2020-01-19 20:08:48 +00:00
async onDeviceConnected ( device ) {
2021-01-21 21:31:37 +01:00
logger . info ( 'onDeviceConnected()' ) ;
logger . debug ( ` Device: ${ device } ` ) ;
2021-01-18 17:21:37 +01:00
if ( ! device ) {
2021-01-21 21:31:37 +01:00
logger . error ( 'Device is null. Should we break/return when this happens?' ) ;
2021-01-18 17:21:37 +01:00
}
2020-01-19 20:08:48 +00:00
const objects = await this . objectManager . GetManagedObjects ( ) ;
2020-01-24 17:30:17 +00:00
const paths = Object . keys ( objects ) ;
2021-01-22 15:49:02 +01:00
const characteristics = [ ] ;
2020-01-19 20:08:48 +00:00
2021-01-22 15:49:02 +01:00
// eslint-disable-next-line no-restricted-syntax
2020-01-24 17:30:17 +00:00
for ( const path of paths ) {
2020-01-19 20:08:48 +00:00
const interfaces = Object . keys ( objects [ path ] ) ;
if ( interfaces . indexOf ( GATT _CHRC _ID ) > - 1 ) {
characteristics . push ( path ) ;
}
2020-01-17 14:50:58 +00:00
}
2021-01-22 15:49:02 +01:00
// eslint-disable-next-line no-restricted-syntax
2020-01-24 17:30:17 +00:00
for ( const path of paths ) {
2020-01-19 20:08:48 +00:00
const interfaces = Object . keys ( objects [ path ] ) ;
if ( interfaces . indexOf ( GATT _SERVICE _ID ) > - 1 ) {
2021-01-22 15:49:02 +01:00
const chPaths = [ ] ;
// eslint-disable-next-line no-restricted-syntax
2020-01-19 20:08:48 +00:00
for ( const c of characteristics ) {
2021-01-22 15:49:02 +01:00
if ( c . startsWith ( ` ${ path } / ` ) ) {
2020-01-19 20:08:48 +00:00
chPaths . push ( c ) ;
}
}
2021-01-22 15:49:02 +01:00
logger . info ( ` trying ${ chPaths . length } characteristics ` ) ;
2020-01-17 14:50:58 +00:00
2020-01-19 20:08:48 +00:00
this . plejdService = await this . _processPlejdService ( path , chPaths ) ;
if ( this . plejdService ) {
break ;
}
}
}
if ( ! this . plejdService ) {
2021-01-22 15:49:02 +01:00
logger . info ( "warning: wasn't able to connect to Plejd, will retry." ) ;
2020-01-20 07:58:23 +00:00
this . emit ( 'connectFailed' ) ;
2020-01-17 14:50:58 +00:00
return ;
}
2020-01-24 10:20:39 +01:00
if ( ! this . characteristics . auth ) {
2021-01-21 21:31:37 +01:00
logger . error ( 'unable to enumerate characteristics.' ) ;
2020-01-24 10:20:39 +01:00
this . emit ( 'connectFailed' ) ;
return ;
}
2021-01-22 15:49:02 +01:00
this . connectedDevice = device . device ;
2020-01-21 14:24:02 +00:00
await this . authenticate ( ) ;
2020-01-17 14:50:58 +00:00
}
2021-01-22 15:49:02 +01:00
// eslint-disable-next-line no-unused-vars
2020-01-20 10:58:03 +00:00
async onLastDataUpdated ( iface , properties , invalidated ) {
2020-01-20 07:58:23 +00:00
if ( iface !== GATT _CHRC _ID ) {
return ;
}
2020-01-20 10:58:03 +00:00
2020-01-20 07:58:23 +00:00
const changedKeys = Object . keys ( properties ) ;
if ( changedKeys . length === 0 ) {
return ;
}
2021-01-22 15:49:02 +01:00
const value = await properties . Value ;
2020-01-20 10:58:03 +00:00
if ( ! value ) {
return ;
}
2020-01-20 07:58:23 +00:00
const data = value . value ;
2020-01-19 20:08:48 +00:00
const decoded = this . _encryptDecrypt ( this . cryptoKey , this . plejdService . addr , data ) ;
2020-01-17 14:50:58 +00:00
2021-01-18 17:21:37 +01:00
const deviceId = parseInt ( decoded [ 0 ] , 10 ) ;
// What is bytes 2-3?
const cmd = decoded . toString ( 'hex' , 3 , 5 ) ;
const state = parseInt ( decoded . toString ( 'hex' , 5 , 6 ) , 10 ) ; // Overflows for command 0x001b, scene command
2021-01-22 15:49:02 +01:00
// eslint-disable-next-line no-bitwise
2021-01-18 17:21:37 +01:00
const data2 = parseInt ( decoded . toString ( 'hex' , 6 , 8 ) , 16 ) >> 8 ;
2020-01-17 14:50:58 +00:00
if ( decoded . length < 5 ) {
2021-01-21 21:31:37 +01:00
logger . debug ( ` Too short raw event ignored: ${ decoded . toString ( 'hex' ) } ` ) ;
2020-01-17 14:50:58 +00:00
// ignore the notification since too small
return ;
}
2021-01-21 21:31:37 +01:00
const deviceName = this . _getDeviceName ( deviceId ) ;
logger . verbose ( ` Raw event received: ${ decoded . toString ( 'hex' ) } ` ) ;
2021-01-22 15:49:02 +01:00
logger . verbose (
` Device ${ deviceId } , cmd ${ cmd . toString ( 'hex' ) } , state ${ state } , dim/data2 ${ data2 } ` ,
) ;
2020-01-17 14:50:58 +00:00
if ( cmd === BLE _CMD _DIM _CHANGE || cmd === BLE _CMD _DIM2 _CHANGE ) {
2021-01-18 17:21:37 +01:00
const dim = data2 ;
2020-01-17 14:50:58 +00:00
2021-01-21 21:31:37 +01:00
logger . debug ( ` ${ deviceName } ( ${ deviceId } ) got state+dim update. S: ${ state } , D: ${ dim } ` ) ;
2020-06-12 11:15:00 +02:00
2021-01-18 17:21:37 +01:00
this . emit ( 'stateChanged' , deviceId , {
2021-01-22 15:49:02 +01:00
state ,
brightness : dim ,
2020-06-12 11:15:00 +02:00
} ) ;
2021-01-18 17:21:37 +01:00
this . plejdDevices [ deviceId ] = {
2021-01-22 15:49:02 +01:00
state ,
dim ,
2021-01-18 17:21:37 +01:00
} ;
2021-01-21 21:31:37 +01:00
logger . verbose ( ` All states: ${ JSON . stringify ( this . plejdDevices ) } ` ) ;
2020-06-12 11:15:00 +02:00
} else if ( cmd === BLE _CMD _STATE _CHANGE ) {
2021-01-21 21:31:37 +01:00
logger . debug ( ` ${ deviceName } ( ${ deviceId } ) got state update. S: ${ state } ` ) ;
2021-01-18 17:21:37 +01:00
this . emit ( 'stateChanged' , deviceId , {
2021-01-22 15:49:02 +01:00
state ,
2020-06-12 11:15:00 +02:00
} ) ;
2021-01-18 17:21:37 +01:00
this . plejdDevices [ deviceId ] = {
2021-01-22 15:49:02 +01:00
state ,
dim : 0 ,
2021-01-18 17:21:37 +01:00
} ;
2021-01-21 21:31:37 +01:00
logger . verbose ( ` All states: ${ this . plejdDevices } ` ) ;
2020-06-12 11:15:00 +02:00
} else if ( cmd === BLE _CMD _SCENE _TRIG ) {
2021-01-18 17:21:37 +01:00
const sceneId = parseInt ( decoded . toString ( 'hex' , 5 , 6 ) , 16 ) ;
const sceneName = this . _getDeviceName ( sceneId ) ;
2020-01-17 14:50:58 +00:00
2021-01-22 15:49:02 +01:00
logger . debug (
` ${ sceneName } ( ${ sceneId } ) scene triggered (device id ${ deviceId } ). Name can be misleading if there is a device with the same numeric id. ` ,
) ;
2021-01-18 17:21:37 +01:00
this . emit ( 'sceneTriggered' , deviceId , sceneId ) ;
2021-01-22 15:49:02 +01:00
} else if ( cmd === '001b' ) {
2021-01-21 21:31:37 +01:00
logger . silly ( 'Command 001b seems to be some kind of often repeating ping/mesh data' ) ;
2021-01-22 15:49:02 +01:00
} else {
2021-01-21 21:31:37 +01:00
logger . verbose ( ` Command ${ cmd . toString ( 'hex' ) } unknown. Device ${ deviceName } ( ${ deviceId } ) ` ) ;
2021-01-18 17:21:37 +01:00
}
2020-01-17 14:50:58 +00:00
}
wireEvents ( ) {
2021-01-21 21:31:37 +01:00
logger . info ( 'wireEvents()' ) ;
2020-01-17 14:50:58 +00:00
const self = this ;
this . on ( 'pingFailed' , this . onPingFailed . bind ( self ) ) ;
this . on ( 'pingSuccess' , this . onPingSuccess . bind ( self ) ) ;
}
2021-01-22 15:49:02 +01:00
// eslint-disable-next-line class-methods-use-this
2020-01-17 14:50:58 +00:00
_createChallengeResponse ( key , challenge ) {
const intermediate = crypto . createHash ( 'sha256' ) . update ( xor ( key , challenge ) ) . digest ( ) ;
const part1 = intermediate . subarray ( 0 , 16 ) ;
const part2 = intermediate . subarray ( 16 ) ;
const resp = xor ( part1 , part2 ) ;
return resp ;
}
2021-01-22 15:49:02 +01:00
// eslint-disable-next-line class-methods-use-this
2020-01-17 14:50:58 +00:00
_encryptDecrypt ( key , addr , data ) {
2021-01-22 15:49:02 +01:00
const buf = Buffer . concat ( [ addr , addr , addr . subarray ( 0 , 4 ) ] ) ;
2020-01-17 14:50:58 +00:00
2021-01-22 15:49:02 +01:00
const cipher = crypto . createCipheriv ( 'aes-128-ecb' , key , '' ) ;
2020-01-17 14:50:58 +00:00
cipher . setAutoPadding ( false ) ;
2021-01-22 15:49:02 +01:00
let ct = cipher . update ( buf ) . toString ( 'hex' ) ;
2020-01-17 14:50:58 +00:00
ct += cipher . final ( ) . toString ( 'hex' ) ;
ct = Buffer . from ( ct , 'hex' ) ;
2021-01-22 15:49:02 +01:00
let output = '' ;
for ( let i = 0 , { length } = data ; i < length ; i ++ ) {
// eslint-disable-next-line no-bitwise
2020-01-17 14:50:58 +00:00
output += String . fromCharCode ( data [ i ] ^ ct [ i % 16 ] ) ;
}
return Buffer . from ( output , 'ascii' ) ;
}
2021-01-18 17:21:37 +01:00
_getDeviceName ( deviceId ) {
2021-01-22 15:49:02 +01:00
return ( this . devices . find ( ( d ) => d . id === deviceId ) || { } ) . name ;
2021-01-18 17:21:37 +01:00
}
2021-01-22 15:49:02 +01:00
// eslint-disable-next-line class-methods-use-this
2020-01-17 14:50:58 +00:00
_reverseBuffer ( src ) {
2021-01-22 15:49:02 +01:00
const buffer = Buffer . allocUnsafe ( src . length ) ;
2020-01-17 14:50:58 +00:00
2021-01-22 15:49:02 +01:00
for ( let i = 0 , j = src . length - 1 ; i <= j ; ++ i , -- j ) {
buffer [ i ] = src [ j ] ;
buffer [ j ] = src [ i ] ;
2020-01-17 14:50:58 +00:00
}
2021-01-22 15:49:02 +01:00
return buffer ;
2020-01-17 14:50:58 +00:00
}
}
2021-01-13 01:47:05 +01:00
module . exports = PlejdService ;