Skip to content

Adapter reported as unsupported on older Windows 10 (e.g. 1909 / build 18363) — IsExtendedAdvertisingSupported / MaxAdvertisementDataLength throw and abort capability reading #84

@lengran57

Description

@lengran57

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

  1. Run on Windows 10 build < 19041 (e.g. 1909 / 18363) with a working BLE adapter.
  2. noble.on('stateChange', s => console.log(s)) then start noble.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions