From 21bf62af951156db92ca2455d80e106c301cd566 Mon Sep 17 00:00:00 2001 From: Piclaw Date: Thu, 23 Apr 2026 09:33:35 -0700 Subject: [PATCH] Fix BLE disconnect crash and event listener leaks - Guard connectionStep() in updateConnected() behind connectDialog.isOpen() to prevent 'Modal has not been opened yet' crash when BLE disconnects during file write operations (fixes #377) - Store bound event handlers in constructor and reuse them so removeEventListener actually removes the old listener - Fix advertisement listener cleanup in connectToBluetoothDevice() --- js/workflows/ble.js | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/js/workflows/ble.js b/js/workflows/ble.js index c2ad3239..91622ca3 100644 --- a/js/workflows/ble.js +++ b/js/workflows/ble.js @@ -34,6 +34,10 @@ class BLEWorkflow extends Workflow { {reconnect: false, request: true}, {reconnect: true, request: true}, ]; + + // Store bound event handlers so they can be properly removed + this._boundOnDisconnected = this.onDisconnected.bind(this); + this._boundOnSerialReceive = this.onSerialReceive.bind(this); } // This is called when a user clicks the main disconnect button @@ -102,8 +106,8 @@ class BLEWorkflow extends Workflow { this.rxCharacteristic = await this.serialService.getCharacteristic(bleNusCharRXUUID); // Remove any existing event listeners to prevent multiple reads - this.txCharacteristic.removeEventListener('characteristicvaluechanged', this.onSerialReceive.bind(this)); - this.txCharacteristic.addEventListener('characteristicvaluechanged', this.onSerialReceive.bind(this)); + this.txCharacteristic.removeEventListener('characteristicvaluechanged', this._boundOnSerialReceive); + this.txCharacteristic.addEventListener('characteristicvaluechanged', this._boundOnSerialReceive); await this.txCharacteristic.startNotifications(); return true; } catch (e) { @@ -144,7 +148,12 @@ class BLEWorkflow extends Workflow { async connectToBluetoothDevice(device) { const abortController = new AbortController(); - async function onAdvertisementReceived(event) { + // Remove previous advertisement listener if one was stored + if (this._boundOnAdvertisementReceived) { + device.removeEventListener('advertisementreceived', this._boundOnAdvertisementReceived); + } + + this._boundOnAdvertisementReceived = (async function onAdvertisementReceived(event) { console.log('> Received advertisement from "' + device.name + '"...'); // Stop watching advertisements to conserve battery life. abortController.abort(); @@ -164,10 +173,9 @@ class BLEWorkflow extends Workflow { } else { console.log('Unable to connect to bluetooth device "' + device.name + '.'); } - } + }).bind(this); - device.removeEventListener('advertisementreceived', onAdvertisementReceived.bind(this)); - device.addEventListener('advertisementreceived', onAdvertisementReceived.bind(this)); + device.addEventListener('advertisementreceived', this._boundOnAdvertisementReceived); this.debugLog("Attempting to connect to " + device.name + "..."); try { @@ -199,8 +207,8 @@ class BLEWorkflow extends Workflow { async switchToDevice(device) { this.bleDevice = device; - this.bleDevice.removeEventListener("gattserverdisconnected", this.onDisconnected.bind(this)); - this.bleDevice.addEventListener("gattserverdisconnected", this.onDisconnected.bind(this)); + this.bleDevice.removeEventListener("gattserverdisconnected", this._boundOnDisconnected); + this.bleDevice.addEventListener("gattserverdisconnected", this._boundOnDisconnected); console.log("connected", this.bleServer); try { @@ -267,7 +275,9 @@ class BLEWorkflow extends Workflow { updateConnected(connectionState) { super.updateConnected(connectionState); - this.connectionStep(2); + if (this.connectDialog && this.connectDialog.isOpen()) { + this.connectionStep(2); + } } async available() {