From df726296fe7b7e8ff23fbe7bed1dd48243ecc1dc Mon Sep 17 00:00:00 2001 From: Heiko Rothe Date: Wed, 16 Dec 2020 12:54:08 +0100 Subject: [PATCH] feat(bluetooth-low-energy): auto-attempt to recover scanning states --- .../bluetooth/bluetooth.service.spec.ts | 25 ++++++++++++++++++- .../bluetooth/bluetooth.service.ts | 23 +++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/integrations/bluetooth/bluetooth.service.spec.ts b/src/integrations/bluetooth/bluetooth.service.spec.ts index 588a23b..d1b0e9f 100644 --- a/src/integrations/bluetooth/bluetooth.service.spec.ts +++ b/src/integrations/bluetooth/bluetooth.service.spec.ts @@ -255,7 +255,7 @@ Requesting information ... it('should only setup noble listeners once', () => { service.onLowEnergyDiscovery(() => undefined); service.onLowEnergyDiscovery(() => undefined); - expect(mockNoble.on).toHaveBeenCalledTimes(4); + expect(mockNoble.on).toHaveBeenCalledTimes(5); }); it('should enable scanning when the adapter is inactive', () => { @@ -420,6 +420,29 @@ Requesting information ... expect(peripheral.disconnectAsync).toHaveBeenCalled(); }); + + it('should restart scanning if nothing has been detected for a while', async () => { + jest.useFakeTimers('modern'); + + // eslint-disable-next-line @typescript-eslint/no-empty-function + service.onLowEnergyDiscovery(() => {}); + const stateCallback = mockNoble.on.mock.calls.find( + (call) => call[0] === 'stateChange' + )[1]; + const discoveryCallback = mockNoble.on.mock.calls.find( + (call) => call[0] === 'discover' + )[1]; + await stateCallback('poweredOn'); + jest.resetAllMocks(); + + discoveryCallback(); + jest.setSystemTime(Date.now() + 31 * 1000); + + await service.verifyLowEnergyScanner(); + + expect(mockNoble.stopScanning).toHaveBeenCalledTimes(1); + expect(mockNoble.startScanningAsync).toHaveBeenCalledTimes(1); + }); }); it('should reset adapters that have been locked for too long', () => { diff --git a/src/integrations/bluetooth/bluetooth.service.ts b/src/integrations/bluetooth/bluetooth.service.ts index 4c8c670..9678b67 100644 --- a/src/integrations/bluetooth/bluetooth.service.ts +++ b/src/integrations/bluetooth/bluetooth.service.ts @@ -11,6 +11,7 @@ import { Interval } from '@nestjs/schedule'; const RSSI_REGEX = new RegExp(/-?[0-9]+/); const INQUIRY_LOCK_TIMEOUT = 30 * 1000; +const SCAN_NO_PERIPHERAL_TIMEOUT = 30 * 1000; const execPromise = util.promisify(exec); @@ -38,6 +39,7 @@ export class BluetoothService { private readonly classicConfig: BluetoothClassicConfig; private readonly adapters = new BluetoothAdapterMap(); private lowEnergyAdapterId: number; + private lastLowEnergyDiscovery: Date; constructor( private readonly configService: ConfigService, @@ -294,6 +296,26 @@ export class BluetoothService { }); } + /** + * Restarts the scanning process if nothing has been detected for a while. + */ + @Interval(5 * 1000) + async verifyLowEnergyScanner(): Promise { + if ( + this.lowEnergyAdapterId != undefined && + this.adapters.getState(this.lowEnergyAdapterId) == 'scan' && + this.lastLowEnergyDiscovery != undefined && + this.lastLowEnergyDiscovery.getTime() < + Date.now() - SCAN_NO_PERIPHERAL_TIMEOUT + ) { + this.logger.warn( + 'Did not detect any low energy advertisements in a while, restarting scanner' + ); + await this.handleAdapterStateChange('poweredOff'); + await this.handleAdapterStateChange('poweredOn'); + } + } + /** * Sets up Noble hooks. */ @@ -301,6 +323,7 @@ export class BluetoothService { this.lowEnergyAdapterId = parseInt(process.env.NOBLE_HCI_DEVICE_ID) || 0; noble.on('stateChange', this.handleAdapterStateChange.bind(this)); + noble.on('discover', () => (this.lastLowEnergyDiscovery = new Date())); noble.on('warning', (message) => { if (message == 'unknown peripheral undefined RSSI update!') { return;