diff --git a/src/bluetooth-extension/index.ts b/src/bluetooth-extension/index.ts index dc3170f..684a774 100644 --- a/src/bluetooth-extension/index.ts +++ b/src/bluetooth-extension/index.ts @@ -16,7 +16,7 @@ import { export namespace CommandIDs { export const openDeviceRegistryDialog = 'bluetooth-manager:open-dialog-for-devices-registry'; - export const disconnectDevice = 'bluetooth-manager:disconnect-device'; + export const disconnect = 'bluetooth-manager:disconnect-device'; } export function buildCompleteIdentifier(native: BluetoothDevice): string { @@ -67,13 +67,13 @@ const BluetoothSidebarPlugin: JupyterFrontEndPlugin = { const openDeviceRegistryDialogLabel = trans.__('Add a Device'); let runningItemsList: Array; - app.commands.addCommand(CommandIDs.disconnectDevice, { + app.commands.addCommand(CommandIDs.disconnect, { execute: args => { const selectedDevice = bluetoothManager.deviceList.find( device => device.native.id === (args.deviceID as string) ); if (selectedDevice) { - bluetoothManager.disconnectDevice(selectedDevice); + bluetoothManager.disconnect(selectedDevice); return selectedDevice; } else { throw new Error('No device provided or device is invalid'); @@ -87,20 +87,24 @@ const BluetoothSidebarPlugin: JupyterFrontEndPlugin = { execute: async () => { showDialog({ title: 'Select device type', - body: new DropDownRegistry(bluetoothManager.registry), + body: new DropDownRegistry(bluetoothManager.deviceTypeRegistry), buttons: [ Dialog.okButton({ label: 'Select' }), Dialog.cancelButton({ label: 'Cancel' }) ] }).then(async result => { if (result.button.accept) { - bluetoothManager.registry.itemsList.forEach(async item => { - if (item.deviceType === result.value) { - await bluetoothManager.connectDevice(item); - } else { - console.warn('There is no corresponding item in the registry!'); + bluetoothManager.deviceTypeRegistry.deviceTypes.forEach( + async item => { + if (item.deviceType === result.value) { + await bluetoothManager.connect(item); + } else { + console.warn( + 'There is no corresponding item in the registry!' + ); + } } - }); + ); } }); } @@ -151,12 +155,12 @@ export class DropDownRegistry extends Widget implements Dialog.IBodyWidget { - constructor(registry: BluetoothManager.DeviceRegistry) { + constructor(registry: BluetoothManager.DeviceTypeRegistry) { super(); this._selectList = document.createElement('select'); this.node.appendChild(this._selectList); this.registry = registry; - registry.itemsList.forEach(item => { + registry.deviceTypes.forEach(item => { const option = document.createElement('option'); option.value = item.deviceType; option.text = item.deviceType; @@ -169,7 +173,7 @@ export class DropDownRegistry } private _selectList: HTMLSelectElement; - public registry: BluetoothManager.DeviceRegistry; + public registry: BluetoothManager.DeviceTypeRegistry; } const BluetoothExtensionPlugins: JupyterFrontEndPlugin[] = [ diff --git a/src/bluetooth/BluetoothDeviceRunningItem.ts b/src/bluetooth/BluetoothDeviceRunningItem.ts index 158794c..3d4373f 100644 --- a/src/bluetooth/BluetoothDeviceRunningItem.ts +++ b/src/bluetooth/BluetoothDeviceRunningItem.ts @@ -5,7 +5,7 @@ import { buildCompleteIdentifier } from '../bluetooth-extension'; import { Menu } from '@lumino/widgets'; import { CommandRegistry } from '@lumino/commands'; -export const disconnectDevice = 'bluetooth-manager:disconnect-device'; +export const disconnect = 'bluetooth-manager:disconnect-device'; export class BluetoothDeviceRunningItem implements IRunningSessions.IRunningItem @@ -58,7 +58,7 @@ export class BluetoothDeviceRunningItem } shutdown() { - this.bluetoothManager.disconnectDevice(this._device); + this.bluetoothManager.disconnect(this._device); } private _device: BluetoothManager.Device; diff --git a/src/bluetooth/BluetoothManager.ts b/src/bluetooth/BluetoothManager.ts index 57a01fd..1b9d4c7 100644 --- a/src/bluetooth/BluetoothManager.ts +++ b/src/bluetooth/BluetoothManager.ts @@ -13,123 +13,116 @@ export class BluetoothManager implements IBluetoothManager { this.deviceListChanged = new Signal>( this ); - this.registeredByAPlugin = new Signal< - this, - BluetoothManager.DeviceRegistry - >(this); - this._registry = new BluetoothManager.DeviceRegistry(); + this._deviceTypeRegistry = new BluetoothManager.DeviceTypeRegistry(); this._deviceList = []; - this._identifierRegistry = []; + this._identifierMap = new Map(); } get deviceList(): Array { return this._deviceList; } - get registry(): BluetoothManager.DeviceRegistry { - return this._registry; + get deviceTypeRegistry(): BluetoothManager.DeviceTypeRegistry { + return this._deviceTypeRegistry; } - get identifierRegistry(): Array { - return this._identifierRegistry; - } - - async connectDevice( - registryItem: IDeviceRegistryItem + async connect( + registryItem: IDeviceTypeRegistryItem ): Promise { - const native = await this.requestDevice(registryItem); - if (native) { + try { + const native = await this.requestDevice(registryItem); + if (!native) { + console.warn('No device selected'); + return undefined; + } const device = await registryItem.factory(native); if (device && device.isConnected) { - this.addDeviceToList(device); - device.disconnected.connect(async () => { - this.removeDeviceFromList(device); + this._addDeviceToList(device); + device.disconnected.connect(() => { + this._removeDeviceFromList(device); }); return device; } + return undefined; + } catch (error) { + console.error('Connection failed:', error); + throw error; } } - async disconnectDevice(device: BluetoothManager.Device) { + async disconnect(device: BluetoothManager.Device) { await device.disconnect(); - device.dispose(); } // Method to add a device to the list - addDeviceToList(device: BluetoothManager.Device): void { + private _addDeviceToList(device: BluetoothManager.Device): void { const identifier = buildCompleteIdentifier(device.native); if (this.identifierRegistry.includes(identifier) === false) { this._deviceList.push(device); - this.identifierRegistry.push(identifier); + this._identifierMap.set(identifier, device); + // Emit the signal when the list changes + this.deviceListChanged.emit(this._deviceList); } else { - console.warn('The device is already in the identifierRegistry'); + console.log('The device is already in the registry of identifiers'); } - // Emit the signal when the list changes - this.deviceListChanged.emit(this._deviceList); } // Method to remove a device from the list - removeDeviceFromList(device: BluetoothManager.Device): void { + private _removeDeviceFromList(device: BluetoothManager.Device): void { const index = this._deviceList.indexOf(device); if (index > -1) { this._deviceList.splice(index, 1); - this.identifierRegistry.splice(index, 1); + const identifier = buildCompleteIdentifier(device.native); + this._identifierMap.delete(identifier); // Emit the signal when the list changes this.deviceListChanged.emit(this._deviceList); } - device.dispose(); + if (!device.isDisposed) { + device.dispose(); + } } - removeAllDevices() { - this._deviceList.forEach((device, index) => { - this.removeDeviceFromList(device); - this.deviceListChanged.emit(this._deviceList); - }); + get identifierRegistry(): Array { + return Array.from(this._identifierMap.keys()); } - register(registryItem: IDeviceRegistryItem) { - this._registry.add(registryItem); - this.registeredByAPlugin.emit(this._registry); - console.warn( - `New item from category ${registryItem.deviceType} is added to the registry.` - ); - return this._registry; + removeAllDevices(deviceList: Array) { + for (const device of deviceList) { + const index = this._deviceList.indexOf(device); + if (index > -1) { + this._deviceList.splice(index, 1); + device.dispose(); + } + } + this.deviceListChanged.emit(this._deviceList); } async checkWebBluetoothSupport(): Promise { - const isWebBluetoothSupported: boolean = navigator.bluetooth ? true : false; - if (isWebBluetoothSupported === false) { + if (!('bluetooth' in navigator)) { showDialog({ title: 'Error', - body: 'Web Bluetooth is not supported on your browser. It works on Chrome and Edge (Firefox and Explorer are not supported). \n Please also check that the Web Bluetooth flag is properly set to enabled in the Chrome flags (chrome://flags/).', + body: 'Web Bluetooth is not supported in your browser. It works on Chrome and Edge. Make sure the Web Bluetooth flag is enabled in chrome://flags/.', buttons: [Dialog.okButton({ label: 'Close' })] }); + return false; } - return isWebBluetoothSupported; + return true; } async requestDevice( - registryItem: IDeviceRegistryItem + registryItem: IDeviceTypeRegistryItem ): Promise { - const isWebBluetoothSupported = await this.checkWebBluetoothSupport(); - if (isWebBluetoothSupported) { - const native = await navigator.bluetooth.requestDevice( - registryItem.options - ); - return native; - } else { - return; + const isSupported = await this.checkWebBluetoothSupport(); + if (!isSupported) { + return undefined; } + return await navigator.bluetooth.requestDevice(registryItem.options); } private _deviceList: Array; public deviceListChanged: Signal>; - public registeredByAPlugin: Signal< - BluetoothManager, - BluetoothManager.DeviceRegistry - >; - private _registry: BluetoothManager.DeviceRegistry; - private _identifierRegistry: Array; + private _deviceTypeRegistry: BluetoothManager.DeviceTypeRegistry; + private _identifierMap: Map; } export namespace BluetoothManager { @@ -160,6 +153,8 @@ export namespace BluetoothManager { this.native.addEventListener('gattserverdisconnected', event => { this.isConnected = false; this.disconnected.emit(true); + this.dispose(); + this.isDisposed = true; }); const server = this.native.gatt; if (server) { @@ -204,35 +199,15 @@ export namespace BluetoothManager { } } - /*async connectAndGetAllServices(): Promise< - Array | undefined - > { - this.native.addEventListener('gattserverdisconnected', event => { - this.isConnected = false; - this.disconnected.emit(true); - }); - const server = this.native.gatt - if (server) { - server.connect(); - if (server.connected === true) { - const services = await server.getPrimaryServices(); - this.isConnected = true; - if (!services || services.length === 0) { - throw new Error('Server exists but no service found on the device.'); - } else { return services; } - } - else { - throw new Error('There is no connection to server. No attempt to get a service.') - } - } - else { - throw new Error('Server is not defined.'); - } - }*/ - async disconnect(): Promise { - if (this.native) { - this.native.gatt?.disconnect(); + if (this.native?.gatt) { + try { + if (this.native.gatt.connected) { + this.native.gatt.disconnect(); + } + } catch (error) { + console.error('Failed to disconnect:', error); + } this.isConnected = false; } } @@ -283,19 +258,27 @@ export namespace BluetoothManager { } } - export class DeviceRegistry implements IDeviceRegistry { - private _registry: Array; - public registryItem: IDeviceRegistryItem; + export class DeviceTypeRegistry implements IDeviceTypeRegistry { constructor() { - this._registry = []; + this._deviceTypes = []; + this._added = new Signal(this); } - add(registryItem: IDeviceRegistryItem) { - this._registry.push(registryItem); + add(registryItem: IDeviceTypeRegistryItem) { + this._deviceTypes.push(registryItem); + this._added.emit(registryItem); } - get itemsList(): Array { - return this._registry; + + get deviceTypes(): IDeviceTypeRegistryItem[] { + return this._deviceTypes; + } + + get added(): Signal { + return this._added; } + + private _deviceTypes: Array; + private _added: Signal; } } @@ -303,23 +286,17 @@ export namespace BluetoothManager { * Interface for the bluetooth manager. */ export interface IBluetoothManager { - addDeviceToList(Device: BluetoothManager.Device): void; - removeDeviceFromList(Device: BluetoothManager.Device): void; - removeAllDevices(Devices: Array): void; - register(registryItem: IDeviceRegistryItem): BluetoothManager.DeviceRegistry; - connectDevice(registryItem: IDeviceRegistryItem): any; - disconnectDevice(device: BluetoothManager.Device): void; + removeAllDevices(deviceList: Array): void; + connect( + registryItem: IDeviceTypeRegistryItem + ): Promise; + disconnect(device: BluetoothManager.Device): void; deviceListChanged: Signal>; - registeredByAPlugin: Signal< - BluetoothManager, - BluetoothManager.DeviceRegistry - >; get deviceList(): Array; - get registry(): BluetoothManager.DeviceRegistry; - get identifierRegistry(): Array; + get deviceTypeRegistry(): BluetoothManager.DeviceTypeRegistry; } -export interface IDeviceRegistryItem { +export interface IDeviceTypeRegistryItem { deviceType: string; factory: ( native: BluetoothDevice @@ -327,9 +304,9 @@ export interface IDeviceRegistryItem { options: IDeviceOptions; } -export interface IDeviceRegistry { - add: (registryItem: IDeviceRegistryItem) => void; - get itemsList(): Array; +export interface IDeviceTypeRegistry { + add: (registryItem: IDeviceTypeRegistryItem) => void; + get deviceTypes(): IDeviceTypeRegistryItem[]; } export const IBluetoothManager = new Token( diff --git a/src/movehub-extension/index.ts b/src/movehub-extension/index.ts index 19b2505..147d40b 100644 --- a/src/movehub-extension/index.ts +++ b/src/movehub-extension/index.ts @@ -6,7 +6,7 @@ import { ITranslator } from '@jupyterlab/translation'; import { IThemeManager, MainAreaWidget } from '@jupyterlab/apputils'; import { Toolbar } from '@jupyterlab/ui-components'; import { - IDeviceRegistryItem, + IDeviceTypeRegistryItem, IBluetoothManager, BluetoothManager } from '../bluetooth/BluetoothManager'; @@ -27,7 +27,7 @@ export const connectMoveHub = 'bluetooth-manager:connect-movehub'; export const disconnectMoveHub = 'bluetooth-manager:disconnect-movehub'; export const moveHubServiceUUID = '00001623-1212-efde-1623-785feabcd123'; export const moveHubCharacteristicUUID = '00001624-1212-efde-1623-785feabcd123'; -export const movehubRegistryItem: IDeviceRegistryItem = { +export const movehubRegistryItem: IDeviceTypeRegistryItem = { deviceType: 'LEGO® Move Hub', options: { acceptAllDevices: false, @@ -56,7 +56,14 @@ const MoveHubRegisterPlugin: JupyterFrontEndPlugin = { bluetoothManager: BluetoothManager ): void => { console.log('JupyterLab move-hub-register plugin is activated!'); - bluetoothManager.register(movehubRegistryItem); + bluetoothManager.deviceTypeRegistry.added.connect( + async (sender, movehubRegistryItem) => { + console.warn( + `New item from category ${movehubRegistryItem.deviceType} is added to the deviceType registry.` + ); + } + ); + bluetoothManager.deviceTypeRegistry.add(movehubRegistryItem); } }; @@ -125,7 +132,7 @@ const LEGOMoveHubControlPanelPlugin: JupyterFrontEndPlugin = { device => device.native.id === (args.deviceID as string) ); if (selectedDevice && selectedDevice instanceof MoveHub) { - bluetoothManager.disconnectDevice(selectedDevice); + bluetoothManager.disconnect(selectedDevice); return selectedDevice; } else { throw new Error('No device provided or device is invalid'); @@ -151,7 +158,7 @@ const LEGOMoveHubControlPanelPlugin: JupyterFrontEndPlugin = { app.commands.addCommand(connectMoveHub, { execute: args => { - const newDevice = bluetoothManager.connectDevice(movehubRegistryItem); + const newDevice = bluetoothManager.connect(movehubRegistryItem); return newDevice; }, caption: 'Connect MoveHub', diff --git a/src/movehub-extension/widget.tsx b/src/movehub-extension/widget.tsx index f5d7dd1..abc9d56 100644 --- a/src/movehub-extension/widget.tsx +++ b/src/movehub-extension/widget.tsx @@ -173,8 +173,9 @@ export class MoveHubModel extends DOMWidgetModel { /*if (!this.movehub.deviceInfo.connected) { console.log('not connected yet');*/ if (identifier === '') { - this.movehub = - await MoveHubModel.bluetoothManager.connectDevice(movehubRegistryItem); + this.movehub = (await MoveHubModel.bluetoothManager.connect( + movehubRegistryItem + )) as MoveHub; } else { const selectedDevice = MoveHubModel.bluetoothManager.deviceList.find( device => device.native.id === identifier