Environment
- Central device: MacBook with Apple Silicon
- Operating system: macOS Sequoia
- Node.js version: v20.12.2
- Library version: @stoprocent/noble v1.19.1
Use case
Implement automatic reconnection for a peripheral device after a disconnection event, such as:
- When the central device’s Bluetooth module is disabled.
- The central device enters sleep mode.
- The peripheral moves out of range, or the peripheral is turned off.
Issue
The library correctly emits a stateChange event with poweredOff during sleep and poweredOn upon waking, but the following issues occur:
- The peripheral’s state remains
connected.
- The peripheral does not trigger a
disconnect event.
- The peripheral stops receiving data or processing read/write requests.
- If the physical peripheral device is turned off after waking, noble logs a warning:
noble: unknown peripheral {connected peripheral UUID} disconnected!
Steps to reproduce
- Connect to a peripheral (code snippet for testing).
- Confirm the peripheral is connected.
- Monitor the following:
- Noble’s
stateChange event
- Peripheral’s
disconnect event
- Peripheral’s connection
state
- Ability to perform peripheral operations
- Put the MacBook into sleep mode (e.g., close the lid for 5–7 seconds or allow it to sleep automatically).
- Wake the MacBook.
- Verify that
stateChange events reflect poweredOff during sleep and poweredOn upon waking.
- Observe whether:
- The
disconnect event is triggered for the peripheral
- The peripheral’s state updates to
disconnected
- Turn off the physical peripheral device.
- Check for any logged warnings.
Expected behavior
- When the MacBook enters sleep mode, the peripheral should trigger a
disconnect event.
- The peripheral’s state should update to
disconnected.
- The peripheral should be fully disconnected.
- No warning messages should be logged.
Actual behavior
- No
disconnect event is triggered when the MacBook enters sleep mode.
- The peripheral’s state remains
connected.
- The peripheral seems to remain connected at the OS level, but this status is not propagated to the noble library.
- Turning off the physical peripheral after waking triggers the warning:
noble: unknown peripheral {connected peripheral UUID} disconnected!
- Noble correctly emits
stateChange events: poweredOff during sleep and poweredOn upon waking.
Additional information
- The issue has been observed across multiple different peripheral devices.
- Testing has been conducted only on macOS due to the unavailability of other operating systems. The issue may occur on other platforms.
Investigation and potential cause
My initial assumption was that the operating system keeps devices connected, while the library marks them as disconnected. I needed to locate the code responsible for cleanup, and I found the following function
|
Noble.prototype.onStateChange = function (state) { |
|
debug(`stateChange ${state}`); |
|
|
|
// If the state is poweredOff and the previous state was poweredOn, clean up the peripherals |
|
if (state === 'poweredOff' && this._state === 'poweredOn') { |
|
this._cleanupPeriperals(); |
|
} |
|
|
|
this._state = state; |
|
this.emit('stateChange', state); |
|
}; |
So, I made several attempts to fix it.
Attempted fix 1
I removed the this._cleanupPeripherals() call from the onStateChange function.
Noble.prototype.onStateChange = function (state) {
debug(`stateChange ${state}`);
// If the state is poweredOff and the previous state was poweredOn, clean up the peripherals
if (state === 'poweredOff' && this._state === 'poweredOn') {
// this._cleanupPeriperals();
}
this._state = state;
this.emit('stateChange', state);
};
This allowed the disconnect event to trigger and correctly updated the peripheral’s state when turning off the physical peripheral. However, if the peripheral remained powered on, its state stayed connected, but no peripheral operations (read, write, subscribe) could be performed.
I assumed that the device connection is not fully operational and requires manual closure. This assumption is supported by the following link.
Attempted fix 2
So I tried forcing disconnection when the state changes to poweredOff:
Noble.prototype.onStateChange = function (state) {
debug(`stateChange ${state}`);
if (state === 'poweredOff' && this._state === 'poweredOn') {
// this._cleanupPeripherals();
for (let peripheral of Object.values(this._peripherals)) {
if (peripheral.state === 'connected') {
peripheral.disconnect();
}
}
}
this._state = state;
this.emit('stateChange', state);
};
This left the peripheral in a disconnecting state which is not ideal.
Attempted fix 3
I modified the logic to trigger disconnect after the state transitions from poweredOff to poweredOn:
Noble.prototype.onStateChange = function (state) {
debug(`stateChange ${state}`);
if (state === 'poweredOn' && this._state === 'poweredOff') {
for (let peripheral of Object.values(this._peripherals)) {
if (peripheral.state === 'connected') {
peripheral.disconnect();
}
}
}
this._state = state;
this.emit('stateChange', state);
};
This approach successfully triggered the disconnect event and updated the peripheral’s state.
Limitations of the fix
Third fix is good for investigation, but it is not production-ready due to the following concerns:
- I haven’t researched how the
allowDuplicates scanning parameter affects this fix, but it feels like it may cause issues.
- Further investigation is needed to determine if this issue occurs on other platforms.
- It may be better to address this at the native code level, such as manually triggering disconnects for each connected device when the state changes to
CBManagerState.poweredOff.
|
- (void)centralManagerDidUpdateState:(CBCentralManager *)central { |
|
auto state = stateToString(central.state); |
|
emit.RadioState(state); |
|
} |
However, this could require JS side updates, as disconnect events might be triggered after the state change. The current implementation clears the peripherals list, which may conflict with this approach and depends on (no. 3).
Thank you! 🙏🙏🙏
I just wanted to say thank you for maintaining this repository. I really appreciate the support and improvements you make to this old library.
✨ ✨ ✨
Environment
Use case
Implement automatic reconnection for a peripheral device after a disconnection event, such as:
Issue
The library correctly emits a
stateChangeevent withpoweredOffduring sleep andpoweredOnupon waking, but the following issues occur:connected.disconnectevent.noble: unknown peripheral {connected peripheral UUID} disconnected!Steps to reproduce
stateChangeeventdisconnecteventstatestateChangeevents reflectpoweredOffduring sleep andpoweredOnupon waking.disconnectevent is triggered for the peripheraldisconnectedExpected behavior
disconnectevent.disconnected.Actual behavior
disconnectevent is triggered when the MacBook enters sleep mode.connected.noble: unknown peripheral {connected peripheral UUID} disconnected!stateChangeevents:poweredOffduring sleep andpoweredOnupon waking.Additional information
Investigation and potential cause
My initial assumption was that the operating system keeps devices connected, while the library marks them as disconnected. I needed to locate the code responsible for cleanup, and I found the following function
noble/lib/noble.js
Lines 90 to 100 in 3a5f04d
So, I made several attempts to fix it.
Attempted fix 1
I removed the
this._cleanupPeripherals()call from theonStateChangefunction.This allowed the
disconnectevent to trigger and correctly updated the peripheral’s state when turning off the physical peripheral. However, if the peripheral remained powered on, its state stayedconnected, but no peripheral operations (read, write, subscribe) could be performed.I assumed that the device connection is not fully operational and requires manual closure. This assumption is supported by the following link.
Attempted fix 2
So I tried forcing disconnection when the state changes to
poweredOff:This left the peripheral in a disconnecting state which is not ideal.
Attempted fix 3
I modified the logic to trigger disconnect after the state transitions from
poweredOfftopoweredOn:This approach successfully triggered the
disconnectevent and updated the peripheral’sstate.Limitations of the fix
Third fix is good for investigation, but it is not production-ready due to the following concerns:
allowDuplicatesscanning parameter affects this fix, but it feels like it may cause issues.CBManagerState.poweredOff.noble/lib/mac/src/ble_manager.mm
Lines 27 to 30 in 3a5f04d
Thank you! 🙏🙏🙏
I just wanted to say thank you for maintaining this repository. I really appreciate the support and improvements you make to this old library.
✨ ✨ ✨