Adapter reported as unsupported on older Windows 10 (e.g. 1909 / build 18363) — IsExtendedAdvertisingSupported / MaxAdvertisementDataLength throw and abort capability reading
Summary
On Windows 10 versions older than 2004 (build 19041) — confirmed on 1909 (build 18363) — @stoprocent/noble always emits stateChange = unsupported, even though the machine has a fully working BLE-capable Bluetooth adapter and BLE works through WinRT.
The root cause is in lib/win/src/radio_watcher.cc: OnRadioChanged() reads two BluetoothAdapter properties that only exist since Windows 10, version 2004 (10.0.19041.0):
BluetoothAdapter.IsExtendedAdvertisingSupported
BluetoothAdapter.MaxAdvertisementDataLength
On older builds these property accessors throw winrt::hresult_error. Because all capability reads share a single try block, that single throw jumps straight to the catch, which invokes the callback with a null radio + empty capabilities. Downstream in ble_manager.cc::OnRadio() a null radio is mapped to AdapterState::Unsupported, so noble reports the adapter as unsupported.
Environment
| Item |
Value |
| noble |
@stoprocent/noble 2.5.3 |
| OS (failing) |
Windows 10 Pro, version 1909, build 18363.1556 |
| OS (working) |
Windows 10 2004+ / Windows 11 (build ≥ 19041) |
| Adapter |
Realtek Bluetooth 5.1 Adapter (driver 1.9.1038.3011) |
| Runtime |
Electron 35 (Node N-API prebuild) |
Root cause (code references)
lib/win/src/radio_watcher.cc, OnRadioChanged():
auto adapter = co_await BluetoothAdapter::GetDefaultAsync();
if (adapter) {
auto radio = co_await adapter.GetRadioAsync();
AdapterCapabilities capabilities;
capabilities.bluetoothAddress = adapter.BluetoothAddress();
capabilities.classicSecureConnectionsSupported = adapter.AreClassicSecureConnectionsSupported();
capabilities.lowEnergySecureConnectionsSupported = adapter.AreLowEnergySecureConnectionsSupported();
capabilities.extendedAdvertisingSupported = adapter.IsExtendedAdvertisingSupported(); // <-- requires Win10 2004 (19041)
capabilities.lowEnergySupported = adapter.IsLowEnergySupported();
capabilities.maxAdvertisementDataLength = adapter.MaxAdvertisementDataLength(); // <-- requires Win10 2004 (19041)
capabilities.peripheralRoleSupported = adapter.IsPeripheralRoleSupported();
capabilities.centralRoleSupported = adapter.IsCentralRoleSupported();
// ...
}
// ...
} catch (const winrt::hresult_error&) {
mRadio = nullptr;
mRadioStateChangedRevoker.revoke();
AdapterCapabilities emptyCapabilities = {};
radioStateChanged(mRadio, emptyCapabilities); // <-- null radio => unsupported
}
lib/win/src/ble_manager.cc, OnRadio():
void BLEManager::OnRadio(Radio& radio, const AdapterCapabilities& capabilities)
{
auto state = AdapterState::Unsupported;
if (radio)
{
state = (AdapterState)radio.State();
}
// null radio => state stays Unsupported => emit "unsupported"
}
API availability
Per the Microsoft docs, these two members were introduced in the Windows 10, version 2004 (10.0.19041.0) contract:
BluetoothAdapter.IsExtendedAdvertisingSupported
BluetoothAdapter.MaxAdvertisementDataLength
All other properties read here (BluetoothAddress, IsLowEnergySupported, IsCentralRoleSupported, IsPeripheralRoleSupported, AreClassicSecureConnectionsSupported, AreLowEnergySecureConnectionsSupported) have been available since 1709, so they succeed on 1909.
Steps to reproduce
- Run on Windows 10 build < 19041 (e.g. 1909 / 18363) with a working BLE adapter.
noble.on('stateChange', s => console.log(s)) then start noble.
- Observe
stateChange is unsupported; scanning never starts.
Expected vs actual
- Expected:
stateChange = poweredOn (the adapter supports BLE + central role).
- Actual:
stateChange = unsupported.
Evidence the adapter is actually fine (WinRT works)
Running the WinRT BluetoothAdapter API directly via PowerShell on the same 1909 machine returns a healthy adapter — only the two newer properties are unavailable:
BluetoothAddress : A65513594A
IsLowEnergySupported : True
IsCentralRoleSupported : True
IsClassicSupported : True
So the hardware/driver/WinRT stack is healthy; the failure is purely the unguarded reads of the two newer properties.
Proposed fix
Read each capability independently and guard the newer properties behind an ApiInformation presence check (plus a per-property try/catch fallback) so that a single unavailable property no longer aborts the whole adapter detection.
#include <winrt/Windows.Foundation.Metadata.h>
using winrt::Windows::Foundation::Metadata::ApiInformation;
namespace {
constexpr auto kBluetoothAdapterTypeName = L"Windows.Devices.Bluetooth.BluetoothAdapter";
template <typename TGetter, typename TValue>
TValue readAdapterCapability(TGetter&& getter, TValue fallback)
{
try { return getter(); }
catch (const winrt::hresult_error&) { return fallback; }
}
bool isAdapterPropertyPresent(const wchar_t* propertyName)
{
try { return ApiInformation::IsPropertyPresent(kBluetoothAdapterTypeName, propertyName); }
catch (const winrt::hresult_error&) { return false; }
}
}
// inside OnRadioChanged(), after `if (adapter)`:
AdapterCapabilities capabilities = {};
capabilities.bluetoothAddress = readAdapterCapability([&] { return adapter.BluetoothAddress(); }, 0ULL);
capabilities.classicSecureConnectionsSupported = readAdapterCapability([&] { return adapter.AreClassicSecureConnectionsSupported(); }, false);
capabilities.lowEnergySecureConnectionsSupported = readAdapterCapability([&] { return adapter.AreLowEnergySecureConnectionsSupported(); }, false);
capabilities.lowEnergySupported = readAdapterCapability([&] { return adapter.IsLowEnergySupported(); }, false);
capabilities.peripheralRoleSupported = readAdapterCapability([&] { return adapter.IsPeripheralRoleSupported(); }, false);
capabilities.centralRoleSupported = readAdapterCapability([&] { return adapter.IsCentralRoleSupported(); }, false);
if (isAdapterPropertyPresent(L"IsExtendedAdvertisingSupported")) {
capabilities.extendedAdvertisingSupported = readAdapterCapability([&] { return adapter.IsExtendedAdvertisingSupported(); }, false);
}
if (isAdapterPropertyPresent(L"MaxAdvertisementDataLength")) {
capabilities.maxAdvertisementDataLength = readAdapterCapability([&] { return adapter.MaxAdvertisementDataLength(); }, 0U);
}
// use the cached value instead of calling the accessor again
Radio bluetooth = nullptr;
if (capabilities.centralRoleSupported) { /* ... */ }
This keeps full behavior on Windows 10 2004+ / Windows 11, while older builds now correctly report poweredOn instead of unsupported.
I'm happy to open a PR with this change if it looks good.
Adapter reported as
unsupportedon older Windows 10 (e.g. 1909 / build 18363) —IsExtendedAdvertisingSupported/MaxAdvertisementDataLengththrow and abort capability readingSummary
On Windows 10 versions older than 2004 (build 19041) — confirmed on 1909 (build 18363) —
@stoprocent/noblealways emitsstateChange = unsupported, even though the machine has a fully working BLE-capable Bluetooth adapter and BLE works through WinRT.The root cause is in
lib/win/src/radio_watcher.cc:OnRadioChanged()reads twoBluetoothAdapterproperties that only exist since Windows 10, version 2004 (10.0.19041.0):BluetoothAdapter.IsExtendedAdvertisingSupportedBluetoothAdapter.MaxAdvertisementDataLengthOn older builds these property accessors throw
winrt::hresult_error. Because all capability reads share a singletryblock, that single throw jumps straight to thecatch, which invokes the callback with a null radio + empty capabilities. Downstream inble_manager.cc::OnRadio()a null radio is mapped toAdapterState::Unsupported, so noble reports the adapter as unsupported.Environment
@stoprocent/noble2.5.3Root cause (code references)
lib/win/src/radio_watcher.cc,OnRadioChanged():lib/win/src/ble_manager.cc,OnRadio():API availability
Per the Microsoft docs, these two members were introduced in the Windows 10, version 2004 (10.0.19041.0) contract:
BluetoothAdapter.IsExtendedAdvertisingSupportedBluetoothAdapter.MaxAdvertisementDataLengthAll other properties read here (
BluetoothAddress,IsLowEnergySupported,IsCentralRoleSupported,IsPeripheralRoleSupported,AreClassicSecureConnectionsSupported,AreLowEnergySecureConnectionsSupported) have been available since 1709, so they succeed on 1909.Steps to reproduce
noble.on('stateChange', s => console.log(s))then start noble.stateChangeisunsupported; scanning never starts.Expected vs actual
stateChange = poweredOn(the adapter supports BLE + central role).stateChange = unsupported.Evidence the adapter is actually fine (WinRT works)
Running the WinRT
BluetoothAdapterAPI directly via PowerShell on the same 1909 machine returns a healthy adapter — only the two newer properties are unavailable:So the hardware/driver/WinRT stack is healthy; the failure is purely the unguarded reads of the two newer properties.
Proposed fix
Read each capability independently and guard the newer properties behind an
ApiInformationpresence check (plus a per-propertytry/catchfallback) so that a single unavailable property no longer aborts the whole adapter detection.This keeps full behavior on Windows 10 2004+ / Windows 11, while older builds now correctly report
poweredOninstead ofunsupported.I'm happy to open a PR with this change if it looks good.