diff --git a/package-lock.json b/package-lock.json index 76d99fd..7e52ace 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,58 +10,28 @@ "integrity": "sha512-rJ8rp+8uzHzkbHwV1mgMxdZCGwtg91+QL77gD6PZUr2YXO2VbVcsd2zxxeAsC4Bvv9I0A4OrUOwgj89iSN0RHQ==" }, "@abandonware/bluetooth-hci-socket": { - "version": "0.5.3-5", - "resolved": "https://registry.npmjs.org/@abandonware/bluetooth-hci-socket/-/bluetooth-hci-socket-0.5.3-5.tgz", - "integrity": "sha512-q9DupPXYcqLyLFrmJqYDaqXoN0fOR4qOZA39dJbEeu1M583Ghr5Bn+JlEAnA6l88DJSBZiyjtCgItDeUfuRwMA==", + "version": "0.5.3-6", + "resolved": "https://registry.npmjs.org/@abandonware/bluetooth-hci-socket/-/bluetooth-hci-socket-0.5.3-6.tgz", + "integrity": "sha512-LwZtu31vgcm6T4GRtHDCye1JZepvjfqW418DYccgcu4podXbBoogkt4NhRP6rnoOxY+49F1Do7oK5K8fModxuw==", "optional": true, "requires": { - "debug": "^4.1.1", - "nan": "^2.14.0", - "node-pre-gyp": "^0.14.0", - "usb": "^1.6.2" + "debug": "^4.2.0", + "nan": "^2.14.1", + "node-pre-gyp": "^0.15.0", + "usb": "^1.6.3" }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "optional": true }, - "node-pre-gyp": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.14.0.tgz", - "integrity": "sha512-+CvDC7ZttU/sSt9rFjix/P05iS43qHCOOGzcr3Ry99bXG7VX953+vFyEuph/tfqoYu8dttBkE86JSKBO2OzcxA==", - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4.4.2" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "optional": true, - "requires": { - "glob": "^7.1.3" - } + "nan": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", + "optional": true } } }, @@ -2428,9 +2398,9 @@ } }, "@mkerix/noble": { - "version": "1.9.2-10.1", - "resolved": "https://registry.npmjs.org/@mkerix/noble/-/noble-1.9.2-10.1.tgz", - "integrity": "sha512-eioE1NzHDJ91WUv4xJajDmsPH5yO1pHIO/3QAzTcs4h88zuWYfxXL+Z76xV1YNqSgWTQ0HlBzjLkAcXSpnMYQg==", + "version": "1.9.2-10.2", + "resolved": "https://registry.npmjs.org/@mkerix/noble/-/noble-1.9.2-10.2.tgz", + "integrity": "sha512-JLyFTm4TBXFuwUDhyMwTgPkj5Kp9Id7/RF3wqOwHfAG/HTDOoShLOxc4UkGcykp6qf9aZFT4Sc9++as70VNbJQ==", "optional": true, "requires": { "@abandonware/bluetooth-hci-socket": "^0.5.3-5", @@ -2440,9 +2410,9 @@ }, "dependencies": { "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "optional": true, "requires": { "ms": "2.1.2" @@ -4533,54 +4503,6 @@ "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "optional": true - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "optional": true - } - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "optional": true - } - } - } } }, "arg": { @@ -14059,9 +13981,9 @@ "dev": true }, "mimic-response": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.0.0.tgz", - "integrity": "sha512-8ilDoEapqA4uQ3TwS0jakGONKXVJqpy+RpM+3b7pLdOjghCrEiGp9SRkFbUHAmZW9vdnrENWHjaweIoTIJExSQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", "optional": true }, "min-document": { @@ -14470,9 +14392,9 @@ "dev": true }, "needle": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.3.3.tgz", - "integrity": "sha512-EkY0GeSq87rWp1hoq/sH/wnTWgFVhYlnIkbJ0YJFfRgEFlz2RraCjBpFQ+vrEgEdp0ThfyHADmkChEhcb7PKyw==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.5.2.tgz", + "integrity": "sha512-LbRIwS9BfkPvNwNHlsA41Q29kL2L/6VaOJ0qisM5lLWsTV3nP15abO5ITL6L81zqFhzjRKDAYjpcBcwM0AVvLQ==", "optional": true, "requires": { "debug": "^3.2.6", @@ -14481,18 +14403,18 @@ }, "dependencies": { "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "optional": true, "requires": { "ms": "^2.1.1" } }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "optional": true } } @@ -14543,9 +14465,9 @@ } }, "node-abi": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.18.0.tgz", - "integrity": "sha512-yi05ZoiuNNEbyT/xXfSySZE+yVnQW6fxPZuFbLyS1s6b5Kw3HzV2PHOM4XR+nsjzkHxByK+2Wg+yCQbe35l8dw==", + "version": "2.19.3", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.19.3.tgz", + "integrity": "sha512-9xZrlyfvKhWme2EXFKQhZRp1yNWT/uI1luYPr3sFl+H4keYY4xR+1jO7mvTTijIsHf1M+QDe9uWuKeEpLInIlg==", "optional": true, "requires": { "semver": "^5.4.1" @@ -14672,6 +14594,35 @@ } } }, + "node-pre-gyp": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.15.0.tgz", + "integrity": "sha512-7QcZa8/fpaU/BKenjcaeFF9hLz2+7S9AqyXFhlH/rilsQ/hPZKK32RtR5EQHJElgu+q5RfbJ34KriI79UWaorA==", + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.3", + "needle": "^2.5.0", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4.4.2" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "optional": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, "node-releases": { "version": "1.1.63", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.63.tgz", @@ -16170,16 +16121,16 @@ "dev": true }, "prebuild-install": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.5.tgz", - "integrity": "sha512-YmMO7dph9CYKi5IR/BzjOJlRzpxGGVo1EsLSUZ0mt/Mq0HWZIHOKHHcHdT69yG54C9m6i45GpItwRHpk0Py7Uw==", + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.6.tgz", + "integrity": "sha512-s8Aai8++QQGi4sSbs/M1Qku62PFK49Jm1CbgXklGz4nmHveDq0wzJkg7Na5QbnO1uNH8K7iqx2EQ/mV0MZEmOg==", "optional": true, "requires": { "detect-libc": "^1.0.3", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", - "mkdirp": "^0.5.1", + "mkdirp-classic": "^0.5.3", "napi-build-utils": "^1.0.1", "node-abi": "^2.7.0", "noop-logger": "^0.1.1", @@ -17695,9 +17646,9 @@ "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" }, "simple-concat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz", - "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", "optional": true }, "simple-get": { @@ -18819,24 +18770,24 @@ } }, "tar-fs": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.0.tgz", - "integrity": "sha512-9uW5iDvrIMCVpvasdFHW0wJPez0K4JnMZtsuIeDI7HyMGJNxmDZDOCQROr7lXyS+iL/QMpj07qcjGYTSdRFXUg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", "optional": true, "requires": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", - "tar-stream": "^2.0.0" + "tar-stream": "^2.1.4" } }, "tar-stream": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.3.tgz", - "integrity": "sha512-Z9yri56Dih8IaK8gncVPx4Wqt86NDmQTSh49XLZgjWpGZL9GK9HKParS2scqHCC4w6X9Gh2jwaU45V47XTKwVA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.4.tgz", + "integrity": "sha512-o3pS2zlG4gxr67GmFYBLlq+dM8gyRGUOvsrHclSkvtVtQbjV0s/+ZE8OpICbaj8clrX3tjeHngYGP7rweaBnuw==", "optional": true, "requires": { - "bl": "^4.0.1", + "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", diff --git a/package.json b/package.json index 47905be..879c962 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,7 @@ "vuepress-plugin-sitemap": "^2.3.1" }, "optionalDependencies": { - "@mkerix/noble": "1.9.2-10.1", + "@mkerix/noble": "1.9.2-10.2", "i2c-bus": "^5.1.0", "mdns": "^2.5.1", "onoff": "^6.0.0" diff --git a/src/integrations/bluetooth-low-energy/bluetooth-low-energy.service.ts b/src/integrations/bluetooth-low-energy/bluetooth-low-energy.service.ts index 3091a00..326b0e2 100644 --- a/src/integrations/bluetooth-low-energy/bluetooth-low-energy.service.ts +++ b/src/integrations/bluetooth-low-energy/bluetooth-low-energy.service.ts @@ -487,19 +487,19 @@ export class BluetoothLowEnergyService this.handleAppDiscovery(tag.id, appId); } catch (e) { - if (e.message === 'timed out') { + if (e.message?.includes('already locked')) { this.logger.debug( - `Temporarily denylisting ${tag.id} from app discovery due to timeout` + `Unable to retrieve companion ID, retrying on next advertisement: ${e.message}` + ); + } else { + this.logger.warn( + `Temporarily denylisting ${tag.id} from app discovery due to error: ${e.message}` ); this.companionAppDenylist.add(tag.id); setTimeout( () => this.companionAppDenylist.delete(tag.id), 3 * 60 * 1000 ); - } else { - this.logger.debug( - `Unable to retrieve companion ID, retrying on next advertisement: ${e.message}` - ); } this.companionAppTags.delete(tag.id); diff --git a/src/integrations/bluetooth/bluetooth.service.spec.ts b/src/integrations/bluetooth/bluetooth.service.spec.ts index f4a6a7c..796a545 100644 --- a/src/integrations/bluetooth/bluetooth.service.spec.ts +++ b/src/integrations/bluetooth/bluetooth.service.spec.ts @@ -4,6 +4,7 @@ const mockNoble = { on: jest.fn(), startScanningAsync: jest.fn(), stopScanning: jest.fn(), + reset: jest.fn(), }; jest.mock( '@mkerix/noble', @@ -18,6 +19,7 @@ import { BluetoothService } from './bluetooth.service'; import { ConfigModule } from '../../config/config.module'; import { BluetoothHealthIndicator } from './bluetooth.health'; import { Peripheral } from '@mkerix/noble'; +import * as Promises from '../../util/promises'; jest.mock('util', () => ({ ...jest.requireActual('util'), @@ -95,13 +97,16 @@ describe('BluetoothService', () => { }); it('should hard reset the HCI device if the command execution times out', async () => { - mockExec.mockRejectedValue({ message: 'timed out' }); + jest.spyOn(Promises, 'sleep').mockResolvedValue(); + mockExec + .mockRejectedValueOnce({ message: 'timed out' }) + .mockResolvedValue({ stdout: '' }); const result = await service.inquireClassicRssi(1, '08:05:90:ed:3b:60'); + expect(result).toBeUndefined(); - expect(mockExec).toHaveBeenCalledWith( - 'hciconfig hci1 down && hciconfig hci1 up' - ); + expect(mockExec).toHaveBeenCalledWith('hciconfig hci1 down'); + expect(mockExec).toHaveBeenCalledWith('hciconfig hci1 up'); }); it('should stop scanning on an adapter while performing an inquiry', async () => { @@ -128,7 +133,7 @@ describe('BluetoothService', () => { it('should start scanning again even after encountering an exception', async () => { service.onLowEnergyDiscovery(() => undefined); const stateChangeHandler = mockNoble.on.mock.calls[0][1]; - stateChangeHandler('poweredOn'); + await stateChangeHandler('poweredOn'); mockExec.mockRejectedValue({ stderr: 'error' }); await service.inquireClassicRssi(0, 'x'); @@ -312,6 +317,28 @@ Requesting information ... }).rejects.toThrow(); }); + it('should throw an exception if trying to connect to an already connecting peripheral', async () => { + const peripheral = { + connectable: true, + state: 'connecting', + } as Peripheral; + + await expect(async () => { + await service.connectLowEnergyDevice(peripheral); + }).rejects.toThrow(); + }); + + it('should re-use connections for already connected peripherals', async () => { + const peripheral = { + connectable: true, + state: 'connected', + } as Peripheral; + + const result = await service.connectLowEnergyDevice(peripheral); + + expect(result).toBe(peripheral); + }); + it('should only allow a single connection on an adapter', async () => { let connectResolve; const connectPromise = new Promise((r) => (connectResolve = r)); @@ -367,7 +394,7 @@ Requesting information ... ); }).rejects.toThrow(); - expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('reset')); + expect(mockNoble.reset).toHaveBeenCalled(); expect(peripheral.removeAllListeners).toHaveBeenCalled(); }); @@ -424,6 +451,19 @@ Requesting information ... expect(peripheral.disconnectAsync).toHaveBeenCalled(); }); + it('should reset the adapter if the disconnect fails', async () => { + const peripheral = { + state: 'connected', + disconnectAsync: jest.fn().mockRejectedValue({ message: '' }), + }; + + await service.disconnectLowEnergyDevice( + (peripheral as unknown) as Peripheral + ); + + expect(mockNoble.reset).toHaveBeenCalled(); + }); + it('should not try to disconnect from a peripheral that is not connected', async () => { const peripheral = { state: 'disconnected', @@ -437,7 +477,8 @@ Requesting information ... expect(peripheral.disconnectAsync).not.toHaveBeenCalled(); }); - it('should restart scanning if nothing has been detected for a while', async () => { + it('should reset adapter if nothing has been detected for a while', async () => { + jest.spyOn(Promises, 'sleep').mockResolvedValue(); jest.useFakeTimers('modern'); // eslint-disable-next-line @typescript-eslint/no-empty-function @@ -456,8 +497,8 @@ Requesting information ... await service.verifyLowEnergyScanner(); - expect(mockNoble.stopScanning).toHaveBeenCalledTimes(1); - expect(mockNoble.startScanningAsync).toHaveBeenCalledTimes(1); + expect(mockExec).toHaveBeenCalledWith('hciconfig hci0 down'); + expect(mockExec).toHaveBeenCalledWith('hciconfig hci0 up'); }); }); diff --git a/src/integrations/bluetooth/bluetooth.service.ts b/src/integrations/bluetooth/bluetooth.service.ts index 8059805..71db3b2 100644 --- a/src/integrations/bluetooth/bluetooth.service.ts +++ b/src/integrations/bluetooth/bluetooth.service.ts @@ -6,7 +6,7 @@ import { BluetoothHealthIndicator } from './bluetooth.health'; import { BluetoothClassicConfig } from '../bluetooth-classic/bluetooth-classic.config'; import { ConfigService } from '../../config/config.service'; import { Device } from '../bluetooth-classic/device'; -import { promiseWithTimeout } from '../../util/promises'; +import { promiseWithTimeout, sleep } from '../../util/promises'; import { Interval } from '@nestjs/schedule'; const RSSI_REGEX = new RegExp(/-?[0-9]+/); @@ -73,6 +73,14 @@ export class BluetoothService { throw new Error('Trying to connect to a non-connectable device'); } + if (peripheral.state === 'connected') { + return peripheral; + } else if (peripheral.state === 'connecting') { + throw new Error( + `Connection to ${peripheral.address} is already trying to be established` + ); + } + this.logger.debug( `Connecting to BLE device at address ${peripheral.address}` ); @@ -100,8 +108,7 @@ export class BluetoothService { ); peripheral.disconnect(); peripheral.removeAllListeners(); - await this.resetHciDevice(this.lowEnergyAdapterId); - this.unlockAdapter(this.lowEnergyAdapterId); + noble.reset(); throw e; } } @@ -126,6 +133,7 @@ export class BluetoothService { `Failed to disconnect from ${peripheral.address}: ${e.message}`, e.trace ); + noble.reset(); } } @@ -152,7 +160,7 @@ export class BluetoothService { killSignal: 'SIGKILL', } ), - this.classicConfig.scanTimeLimit * 1000 * 1.5 + this.classicConfig.scanTimeLimit * 1000 * 2 ); const matches = output.stdout.match(RSSI_REGEX); @@ -248,9 +256,9 @@ export class BluetoothService { */ protected async hardResetHciDevice(adapterId: number): Promise { try { - await execPromise( - `hciconfig hci${adapterId} down && hciconfig hci${adapterId} up` - ); + await execPromise(`hciconfig hci${adapterId} down`); + await sleep(1200); + await execPromise(`hciconfig hci${adapterId} up`); } catch (e) { this.logger.error(e.message); } @@ -285,6 +293,10 @@ export class BluetoothService { * @param adapterId - HCI Device ID of the adapter to unlock */ async unlockAdapter(adapterId: number): Promise { + if (this.adapters.getState(this.lowEnergyAdapterId) != 'inquiry') { + return; + } + this.logger.debug(`Unlocking adapter ${adapterId}`); this.adapters.setState(adapterId, 'inactive'); @@ -320,16 +332,17 @@ export class BluetoothService { async verifyLowEnergyScanner(): Promise { if ( this.lowEnergyAdapterId != undefined && - this.adapters.getState(this.lowEnergyAdapterId) == 'scan' && + ['scan', 'inactive'].includes( + this.adapters.getState(this.lowEnergyAdapterId) + ) && 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' + 'Did not detect any low energy advertisements in a while, resetting' ); - await this.handleAdapterStateChange('poweredOff'); - await this.handleAdapterStateChange('poweredOn'); + await this.hardResetHciDevice(this.lowEnergyAdapterId); } } @@ -338,6 +351,7 @@ export class BluetoothService { */ private setupNoble(): void { this.lowEnergyAdapterId = parseInt(process.env.NOBLE_HCI_DEVICE_ID) || 0; + this.adapters.setState(this.lowEnergyAdapterId, 'inactive'); noble.on('stateChange', this.handleAdapterStateChange.bind(this)); noble.on('discover', () => (this.lastLowEnergyDiscovery = new Date())); @@ -356,20 +370,26 @@ export class BluetoothService { * @param state - State of the HCI adapter */ private async handleAdapterStateChange(state: string): Promise { - if (this.adapters.getState(this.lowEnergyAdapterId) != 'inquiry') { - if (state === 'poweredOn') { + const adapterState = this.adapters.getState(this.lowEnergyAdapterId); + if (state === 'poweredOn') { + if (adapterState == 'inactive') { this.logger.debug( `Start scanning for BLE peripherals on adapter ${this.lowEnergyAdapterId}` ); - await noble.startScanningAsync([], true); - this.adapters.setState(this.lowEnergyAdapterId, 'scan'); - } else { - this.logger.debug( - `Stop scanning for BLE peripherals on adapter ${this.lowEnergyAdapterId}` - ); - await noble.stopScanning(); - this.adapters.setState(this.lowEnergyAdapterId, 'inactive'); + + try { + await promiseWithTimeout(noble.startScanningAsync([], true), 3000); + this.adapters.setState(this.lowEnergyAdapterId, 'scan'); + } catch (e) { + this.logger.error(`Failed to start scanning: ${e.message}`, e.stack); + await this.hardResetHciDevice(this.lowEnergyAdapterId); + } } + } else if (adapterState === 'scan') { + this.logger.debug( + `Adapter ${this.lowEnergyAdapterId} went into state ${state}, cannot continue scanning` + ); + this.adapters.setState(this.lowEnergyAdapterId, 'inactive'); } } } diff --git a/src/util/promises.ts b/src/util/promises.ts index e89ed13..04de2e2 100644 --- a/src/util/promises.ts +++ b/src/util/promises.ts @@ -12,3 +12,9 @@ export function promiseWithTimeout( return result; }); } + +export async function sleep(ms: number): Promise { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +}