From f63995a71b2e19a7b7411409d69d77a4f1acd888 Mon Sep 17 00:00:00 2001 From: TobiasRoeddiger Date: Mon, 2 Oct 2023 15:13:34 +0200 Subject: [PATCH 1/2] added simple mutex to mitigate issues with multipel calls to gatt --- assets/js/ConnectionManager.js | 45 ++++-- assets/js/LEDManager.js | 2 +- assets/js/OpenEarable.js | 245 +++++++++++++++++++++++++-------- assets/js/SensorManager.js | 26 ++-- index.html | 18 ++- 5 files changed, 250 insertions(+), 86 deletions(-) diff --git a/assets/js/ConnectionManager.js b/assets/js/ConnectionManager.js index 1504e33..b621720 100644 --- a/assets/js/ConnectionManager.js +++ b/assets/js/ConnectionManager.js @@ -1,14 +1,13 @@ var openEarable = new OpenEarable(); openEarable.bleManager.subscribeOnConnected(async () => { - $('#connectDeviceButton').hide() - $('#disconnectDeviceButton').show() - $(".is-connect-enabled").prop('disabled', false); // Get device identifier and generation after connected const firmwareVersion = await openEarable.readFirmwareVersion(); const hardwareVersion = await openEarable.readHardwareVersion(); + $('#disconnectDeviceButton').prop('disabled', false); + // Update the DOM with the obtained values $('#fwVersion').text(firmwareVersion); $('#deviceVersion').text(hardwareVersion); @@ -21,29 +20,57 @@ openEarable.bleManager.subscribeOnConnected(async () => { openEarable.bleManager.subscribeOnDisconnected(() => { $('#disconnectDeviceButton').hide() $('#connectDeviceButton').show() - $(".is-connect-enabled").prop('disabled', true); - $('#batteryLevel').hide(); + $("#connectDeviceButton").prop('disabled', false); + $('#batteryLevel').text("XX") + $('#batteryChargingIndicator').hide(); + $('#batteryChargedIndicator').hide(); log("OpenEarable disconnected.", type = "WARNING") // Reset the values to default when disconnected - $('#connectedDevice').text("OpenEarable not connected"); + $('#connectedDevice').text("OpenEarable-XXXX"); $('#fwVersion').text("X.X.X"); $('#deviceVersion').text("X.X.X"); }); openEarable.subscribeBatteryLevelChanged((batteryLevel) => { - $('#batteryLevel').text(' (' + batteryLevel + '%)'); + $('#connectDeviceButton').hide() + $('#disconnectDeviceButton').show() + $(".is-connect-enabled").prop('disabled', false); + + $('#batteryLevel').text(batteryLevel); $('#batteryLevel').show(); }) -$('#connectDeviceButton').click(() => { +openEarable.subscribeBatteryStateChanged((batteryState) => { + if (batteryState === 1) { + $('#batteryChargingIndicator').show(); + $('#batteryChargedIndicator').hide(); + } else if (batteryState === 2) { + $('#batteryChargedIndicator').show(); + $('#batteryChargingIndicator').hide(); + } else { + $('#batteryChargingIndicator').hide(); + $('#batteryChargedIndicator').hide(); + } + +}) + +$('#connectDeviceButton').click(async () => { + $('#connectDeviceButton').prop('disabled', true); log("Scanning for OpenEarables. Please select.", type = "MESSAGE") - openEarable.bleManager.connect(); + try { + await openEarable.bleManager.connect(); + } catch (e) { + $('#connectDeviceButton').prop('disabled', false); + } + }); $('#disconnectDeviceButton').click(() => { + $(".is-connect-enabled").prop('disabled', true); + $('#disconnectDeviceButton').prop('disabled', true); log("Disconnecting OpenEarable.", type = "MESSAGE") openEarable.bleManager.disconnect(); }); diff --git a/assets/js/LEDManager.js b/assets/js/LEDManager.js index fbcd234..fa4bcd9 100644 --- a/assets/js/LEDManager.js +++ b/assets/js/LEDManager.js @@ -67,7 +67,7 @@ function startRainbowMode() { h += increment; if (h > 1) h = 0; // Reset hue value - }, 100); // Adjust interval for speed as well, e.g., 100ms + }, 300); // Adjust interval for speed as well, e.g., 100ms } function stopRainbowMode() { diff --git a/assets/js/OpenEarable.js b/assets/js/OpenEarable.js index 6f591e1..40a7c8b 100644 --- a/assets/js/OpenEarable.js +++ b/assets/js/OpenEarable.js @@ -1,3 +1,109 @@ +/** + * Enumeration of different battery states. + */ +const BATTERY_STATE = { + CHARGING: 0, + CHARGED: 1, + NOT_CHARGING: 2 +} + +/** + * Enumeration of different sensor ids. + */ +const SENSOR_ID = { + IMU: 0, + PRESSURE_SENSOR: 1, + MICROPHONE: 2 +} + +/** + * Enumeration for different audio states. + */ +const AUDIO_STATE = { + IDLE: 0, + PLAY: 1, + PAUSE: 2, + STOP: 3 +}; + +/** + * Enumeration for different jingles. + */ +const JINGLE = { + IDLE: 0, + NOTIFICATION: 1, + SUCCESS: 2, + ERROR: 3, + ALARM: 4, + PING: 5, + OPEN: 6, + CLOSE: 7 +}; + +/** + * Enumeration for different wave types. + */ +const WAVE_TYPE = { + IDLE: 0, + SINE: 1, + TRIANGLE: 2, + SQUARE: 3, + SAW: 4 +}; + +/** + * Dictionary of the different OpenEarable BLE services. + */ +const SERVICES = { + DEVICE_INFO_SERVICE: { + UUID: '45622510-6468-465a-b141-0b9b0f96b468', + CHARACTERISTICS: { + + } + }, + BATTERY_SERVICE: { + UUID: '0000180f-0000-1000-8000-00805f9b34fb', + CHARACTERISTICS: { + BATTERY_LEVEL_CHARACTERISTIC: { + UUID: '00002a19-0000-1000-8000-00805f9b34fb' + }, + BATTERY_STATE_CHARACTERISTIC: { + UUID: '00002a1a-0000-1000-8000-00805f9b34fb' + } + } + }, + PARSE_INFO_SERVICE: { + UUID: 'caa25cb7-7e1b-44f2-adc9-e8c06c9ced43', + CHARACTERISTICS: { + + } + }, + SENSOR_SERVICE: { + UUID: '34c2e3bb-34aa-11eb-adc1-0242ac120002', + CHARACTERISTICS: { + + } + }, + BUTTON_SERVICE: { + UUID: '29c10bdc-4773-11ee-be56-0242ac120002', + CHARACTERISTICS: { + + } + }, + LED_SERVICE: { + UUID: '81040a2e-4819-11ee-be56-0242ac120002', + CHARACTERISTICS: { + + } + }, + AUDIO_SERVICE: { + UUID: '5669146e-476d-11ee-be56-0242ac120002', + CHARACTERISTICS: { + + } + } +} + class OpenEarable { constructor() { this.bleManager = new BLEManager(); @@ -37,33 +143,42 @@ class OpenEarable { } notifyBatteryLevelChanged(value) { + if (!value) return; + const batteryLevel = new DataView(value.buffer).getUint8(0); this.batteryLevelChangedSubscribers.forEach(callback => callback(batteryLevel)); } + notifyBatteryStateChanged(value) { + if (!value) return; + + const batteryState = new DataView(value.buffer).getUint8(0); + this.batteryStateChangedSubscribers.forEach(callback => callback(batteryState)); + } + async onDeviceReady() { - const value = await this.bleManager.readCharacteristic('0000180f-0000-1000-8000-00805f9b34fb', '00002a19-0000-1000-8000-00805f9b34fb'); - this.notifyBatteryLevelChanged(value); + const batteryLevelValue = await this.bleManager.readCharacteristic(SERVICES.BATTERY_SERVICE.UUID, SERVICES.BATTERY_SERVICE.CHARACTERISTICS.BATTERY_LEVEL_CHARACTERISTIC.UUID); + this.notifyBatteryLevelChanged(batteryLevelValue); await this.bleManager.subscribeCharacteristicNotifications( - '0000180f-0000-1000-8000-00805f9b34fb', - '00002a19-0000-1000-8000-00805f9b34fb', + SERVICES.BATTERY_SERVICE.UUID, + SERVICES.BATTERY_SERVICE.CHARACTERISTICS.BATTERY_LEVEL_CHARACTERISTIC.UUID, (notificationEvent) => { this.notifyBatteryLevelChanged(notificationEvent.srcElement.value); } ); - /* - const chargingState = await this.bleManager.readCharacteristic('0000180f-0000-1000-8000-00805f9b34fb', 'placeholder-charging-state-uuid'); - this.batteryStateChangedSubscribers.forEach(callback => callback(new TextDecoder().decode(chargingState))); + + const batteryStateValue = await this.bleManager.readCharacteristic(SERVICES.BATTERY_SERVICE.UUID, SERVICES.BATTERY_SERVICE.CHARACTERISTICS.BATTERY_STATE_CHARACTERISTIC.UUID); + this.notifyBatteryStateChanged(batteryStateValue); await this.bleManager.subscribeCharacteristicNotifications( - '0000180f-0000-1000-8000-00805f9b34fb', - 'placeholder-charging-state-uuid', - (value) => { - this.batteryStateChangedSubscribers.forEach(callback => callback(new TextDecoder().decode(value))); + SERVICES.BATTERY_SERVICE.UUID, + SERVICES.BATTERY_SERVICE.CHARACTERISTICS.BATTERY_STATE_CHARACTERISTIC.UUID, + (notificationEvent) => { + this.notifyBatteryStateChanged(notificationEvent.srcElement.value); } - );*/ + ); this.sensorManager.init(); } @@ -75,39 +190,56 @@ class BLEManager { this.gattServer = null; this.onConnectedSubscribers = []; this.onDisconnectedSubscribers = []; + this.queue = []; + this.operationInProgress = false; } - async connect() { + async _executeNextOperation() { + if (this.queue.length === 0 || this.operationInProgress) { + return; + } + const nextOperation = this.queue.shift(); + this.operationInProgress = true; try { + await nextOperation(); + } catch (error) { + console.error('Error during GATT operation:', error); + } finally { + this.operationInProgress = false; + this._executeNextOperation(); + } + } + + async _enqueueOperation(operation) { + return new Promise((resolve, reject) => { + this.queue.push(async () => { + try { + const result = await operation(); + resolve(result); + } catch (error) { + reject(error); + } + }); + this._executeNextOperation(); + }); + } + async connect() { + return this._enqueueOperation(async () => { this.device = await navigator.bluetooth.requestDevice({ acceptAllDevices: true, - optionalServices: [ - '45622510-6468-465a-b141-0b9b0f96b468', // Device Info Service - '81040a2e-4819-11ee-be56-0242ac120002', // LED Service - '34c2e3bb-34aa-11eb-adc1-0242ac120002', // Sensor Service - '29c10bdc-4773-11ee-be56-0242ac120002', // Button Service, remove? - '0000180f-0000-1000-8000-00805f9b34fb', // Battery Service - 'caa25cb7-7e1b-44f2-adc9-e8c06c9ced43', // Parse Info Service - '5669146e-476d-11ee-be56-0242ac120002' // Audio Service - ] + optionalServices: Object.keys(SERVICES).map((service) => SERVICES[service].UUID) }); this.gattServer = await this.device.gatt.connect(); - - this.device.addEventListener('gattserverdisconnected', () => { - this.cleanup(); - this.notifyAll(this.onDisconnectedSubscribers); - }); - + this.device.addEventListener('gattserverdisconnected', this.handleDisconnected.bind(this)); this.notifyAll(this.onConnectedSubscribers); - - } catch (error) { - console.error("Error connecting to BLE device:", error); - } + }); } + subscribeOnConnected(callback) { this.onConnectedSubscribers.push(callback); } + subscribeOnDisconnected(callback) { this.onDisconnectedSubscribers.push(callback); @@ -126,49 +258,56 @@ class BLEManager { } cleanup() { + if (this.device) { + this.device.removeEventListener('gattserverdisconnected', this.handleDisconnected); + } this.device = null; this.gattServer = null; } + handleDisconnected() { + this.cleanup(); + setTimeout(() => { + this.notifyAll(this.onDisconnectedSubscribers); + }, + 5000); // make sure that system has cleaned up everything by delaying a bit + + } + + ensureConnected() { - if (!this.device || !this.device.gatt.connected) { + if (!this.device || !this.gattServer || !this.device.gatt.connected) { throw new Error("No BLE device connected."); } } async readCharacteristic(serviceUUID, characteristicUUID) { - this.ensureConnected(); - try { + return this._enqueueOperation(async () => { + this.ensureConnected(); const service = await this.gattServer.getPrimaryService(serviceUUID); const characteristic = await service.getCharacteristic(characteristicUUID); const value = await characteristic.readValue(); return value; - } catch (error) { - console.error('Error reading value:', error); - } + }); } async writeCharacteristic(serviceUUID, characteristicUUID, data) { - this.ensureConnected(); - try { + return this._enqueueOperation(async () => { + this.ensureConnected(); const service = await this.gattServer.getPrimaryService(serviceUUID); const characteristic = await service.getCharacteristic(characteristicUUID); await characteristic.writeValue(data); - } catch (error) { - console.error('Error writing value:', error); - } + }); } async subscribeCharacteristicNotifications(serviceUUID, characteristicUUID, callback) { - this.ensureConnected(); - try { + return this._enqueueOperation(async () => { + this.ensureConnected(); const service = await this.gattServer.getPrimaryService(serviceUUID); const characteristic = await service.getCharacteristic(characteristicUUID); await characteristic.startNotifications(); characteristic.addEventListener('characteristicvaluechanged', callback); - } catch (error) { - console.error('Error subscribing to characteristic:', error); - } + }); } } @@ -393,15 +532,7 @@ class OpenEarableSensorConfig { } -/** - * Enumeration for different audio states. - */ -const AUDIO_STATE = { - IDLE: 0, - PLAY: 1, - PAUSE: 2, - STOP: 3 -}; + /** * Represents an audio player that communicates with BLE devices. diff --git a/assets/js/SensorManager.js b/assets/js/SensorManager.js index f2dc0c0..a2efaa9 100644 --- a/assets/js/SensorManager.js +++ b/assets/js/SensorManager.js @@ -1,48 +1,48 @@ $(document).ready(function () { - $('#setSensorConfigurationButton').on('click', function() { + $('#setSensorConfigurationButton').on('click', async function() { console.log("set sensor config") // Check if the checkbox for the first set of sensors is checked if ($('#areSensorsEnabled').is(':checked')) { var sensorSamplingRate = $('#sensorSamplingRate').val(); log("Setting sampling rate for IMU: " + sensorSamplingRate + " Hz") - openEarable.sensorManager.writeSensorConfig(0, sensorSamplingRate, 0); + await openEarable.sensorManager.writeSensorConfig(0, sensorSamplingRate, 0); } else { // If the checkbox is not checked, set the sampling rate to 0 log("Setting IMU disabled.") - openEarable.sensorManager.writeSensorConfig(0, 0, 0); + await openEarable.sensorManager.writeSensorConfig(0, 0, 0); } if ($('#isPressureSensorEnabled').is(':checked')) { var pressureSensorSamplingRate = $('#pressureSensorSamplingRate').val(); log("Setting sampling rate for pressure sensor: " + pressureSensorSamplingRate + " Hz") - openEarable.sensorManager.writeSensorConfig(1, pressureSensorSamplingRate, 0); + await openEarable.sensorManager.writeSensorConfig(1, pressureSensorSamplingRate, 0); } else { log("Setting pressure sensor disabled.") - openEarable.sensorManager.writeSensorConfig(1, 0, 0); + await openEarable.sensorManager.writeSensorConfig(1, 0, 0); } // Check if the checkbox for the microphone is checked if ($('#isMicEnabled').is(':checked')) { var microphoneSamplingRate = $('#microphoneSamplingRate').val(); log("Setting sampling rate for microphone: " + microphoneSamplingRate + " Hz") - openEarable.sensorManager.writeSensorConfig(2, microphoneSamplingRate, 0); + await openEarable.sensorManager.writeSensorConfig(2, microphoneSamplingRate, 0); } else { // If the checkbox is not checked, set the sampling rate to 0 log("Setting microphone disabled.") - openEarable.sensorManager.writeSensorConfig(2, 0, 0); + await openEarable.sensorManager.writeSensorConfig(2, 0, 0); } }); - $('.btn-disable-sensors').on('click', function() { + $('.btn-disable-sensors').on('click', async function() { // Set the sampling rate to 0 for all sensors - openEarable.sensorManager.writeSensorConfig(0, 0, 0); - openEarable.sensorManager.writeSensorConfig(1, 0, 0); - openEarable.sensorManager.writeSensorConfig(2, 0, 0); + await openEarable.sensorManager.writeSensorConfig(0, 0, 0); + await openEarable.sensorManager.writeSensorConfig(1, 0, 0); + await openEarable.sensorManager.writeSensorConfig(2, 0, 0); // Uncheck the checkboxes - $('#areSensorsEnabled, #isMicEnabled').prop('checked', false); + $('#areSensorsEnabled, #isMicEnabled, #isPressureSensorEnabled').prop('checked', false); // Reset the dropdowns to 0 - $('#sensorSamplingRate, #microphoneSamplingRate').val('0'); + $('#sensorSamplingRate, #microphoneSamplingRate, #pressureSensorSamplingRate').val('0'); }); }); diff --git a/index.html b/index.html index 4826b9d..fb2d35b 100644 --- a/index.html +++ b/index.html @@ -176,8 +176,8 @@
Device Connection
OpenEarable-XXXX (XX%)
+ style="font-family: monospace;">OpenEarable-XXXX (XX%)
Firmware X.X.X Device X.X.X
@@ -308,10 +308,13 @@
Audio Control
@@ -583,6 +586,9 @@
Recorder