WiFi Scanning Part 1:Scanning on Windows

I’ve enjoyed my experiments with making my own WiFi based location system. I’ll be writing on it more, but before I do I wanted to turn some attention to the WiFi scanning itself. This is fairly easy to do on both Windows and Android. I won’t be discussing iOS because at the time of this writing, iOS doesn’t allow user applications to perform WiFi scanning (the devices do it themselves and support WiFi based location, but do not expose the lower level functionality to the developers). In this first post I discuss WiFi scanning on Windows.

WiFi Scanning on Windows

Windows in my opinion was the easiest system on which to perform the scanning. An application initiates the scan and the operating system takes care of most of the rest. Is the application tries to retrieve the information a bit later, it’s there. While some might be tempted to request a scan, add a delay, and then retrieve the results, don’t do this. There a number of reasons why, including you can’t really know how long the scan will actually take. Windows also allows a callback function to be registered to receive notifications on operations. It is better to register a callback to be notified when the WiFi scanning is complete. You can see the full source code for how to perform the scanning here. Most of the rest of this is an explanation of the code.

Wireless operations start with requesting a HANDLE that is used to track request and operations. The Windows function WlanOpenHandle() will return this handle. Hold onto it until your application is either closing or nolonger needs to perform wireless operations. When you are done with the HANDLE, release it with WlanCloseHandle().

Once you have your HANDLE, use it to register a notification callback with WlanRegisterNotification. When you want to unregister a callback, call this same function again passing NULL in place of the callback function.

    if (ERROR_SUCCESS == WlanOpenHandle(2, nullptr, &version, &context.wlanHandle))
        result = WlanRegisterNotification(context.wlanHandle, WLAN_NOTIFICATION_SOURCE_ACM, 
                                          TRUE, (WLAN_NOTIFICATION_CALLBACK)WlanNotificationCallback, 
                                          &context, NULL, NULL);
        // Other wireless operations go here.
         WlanRegisterNotification(context.wlanHandle, WLAN_NOTIFICATION_SOURCE_ACM, 
                                  TRUE, NULL, NULL, NULL, NULL);
         WlanCloseHandle(context.wlanHandle, NULL);

Enumerating the Wireless Adapters

I’ll talk in detail about the implementation of the callback function in a moment. A device could have 0 or more wireless adapters. We could request a wireless scan on each of the adapters. For my sample program, it will perform a scan on each adapter one at a time. We can get a list of all the wireless adapters in a single call. The function accepts the address of a variable that will hold a pointer to the returned data. The call to WLanEnumInterfaces takes care of allocating the memory for this information. When we are done with it, we need to deallocate the memory ourselves with a call wo WlanFreeMemory. Enumerating through the array, each element has a property named isState. If the state is equal to the constant wlan_interface_state_connected then the wireless adapter is connected to a network. I’m only scanning when an adapter is being used and connected to a network. My reasons for this is that I ended up using this in diagnostics of some connectivity problems on some remote machines and I was only interested in the adapters being used.

The actual scanning is performed in the call to WlanScan. After the call, I reset a Windows Event object (created earlier in the program, but unused until now) and then wait for the object to have a signaled state with the function WaitForSingleObject. If you are familiar with Windows synchronization objects, then take note this is how I am coordinating code in the main thread with the callback.

if (ERROR_SUCCESS == (result = WlanEnumInterfaces(context.wlanHandle, NULL, &interfaceList)))
    std::cout << "Host, BSSID, Access Point Name, Frequency, RSSI, Capabilities, Rateset, Host Timestamp, Timestamp, BSS Type" << std::endl;

    for (int i = 0; i < (int)interfaceList->dwNumberOfItems; i++)
        PWLAN_INTERFACE_INFO wirelessInterface;
        wirelessInterface = (WLAN_INTERFACE_INFO*)&interfaceList->InterfaceInfo[i];
        if (wirelessInterface->isState == wlan_interface_state_connected)
            context.interfaceGuid = wirelessInterface->InterfaceGuid;
             if (ERROR_SUCCESS != (result = WlanScan(context.wlanHandle, &context.interfaceGuid, NULL, NULL, NULL)))
                std::cout << "Scan failed" << std::endl;
                retVal = 1;
                WaitForSingleObject(context.scanCompleteEvent, INFINITE);

For those not familiar, the call to WaitForSingleObject will cause the code to block until some other thread calls SetEvent on the same object. The callback that I registered will call SetEvent after it has received and process the scan information. This frees the main code to continue its processing.

Receiving the Response

I’m primary interested in printing out some attributes about each access point that is found in a format that is CSV friendly. If the notification received is for a WLAN_NOTIFICATION_SOURCE_ACM event, then that means that the scan information is available. A call to WlanGetNetworkBssList returns the information in a structure in memory allocated for us. After we get done processing this information, we need to release the memory with WlanFreeMemory(). Most of what I do with the information is direct printing of the values. I do have a function to format the BSSID information as a colon delimited string of hexadecimal digits. Information on the capabilities for the access points is stored in bit fields, which I extract and print as string. After iterating through each item in the returned information and printing the comma delimited fields, I call SetEvent so that the main thread can continue executing.

void WlanNotificationCallback(PWLAN_NOTIFICATION_DATA notificationData, PVOID contextData)
    DWORD result;
    PWlanCallbackContext context = (PWlanCallbackContext)contextData;
    switch (notificationData->NotificationSource)

        result = WlanGetNetworkBssList(context->wlanHandle, &context->interfaceGuid,
            NULL /*&pConnectInfo->wlanAssociationAttributes.dot11Ssid */,
            TRUE, NULL, &pBssList);
        if (ERROR_SUCCESS == result)
            for (auto i = 0; i < pBssList->dwNumberOfItems; ++i)
                auto item = pBssList->wlanBssEntries[i];
                std::cout << context->ComputerName << ", ";
                std::cout << FormatBssid(item.dot11Bssid) << ", ";
                std::cout << item.dot11Ssid.ucSSID << ", ";
                std::cout << item.ulChCenterFrequency << ", ";
                std::cout << item.lRssi << ", ";
                std::cout << ((item.usCapabilityInformation & 0x01) ? "[+ESS]" : "[-ESS]");
                std::cout << ((item.usCapabilityInformation & 0x02) ? "[+IBSS]" : "[-IBSS]");
                std::cout << ((item.usCapabilityInformation & 0x04) ? "[+CF_Pollable]" : "[-CF_Pollable]");
                std::cout << ((item.usCapabilityInformation & 0x08) ? "[+CF_PollRequest]" : "[-CF_PollRequest]");
                std::cout << ((item.usCapabilityInformation & 0x10) ? "[+Privacy]" : "[-Privacy]");
                std::cout << ", ";
                for (int k = 0; k < item.wlanRateSet.uRateSetLength; ++k)
                    std::cout << "[" << item.wlanRateSet.usRateSet[k] << "]";
                std::cout << ", ";
                std::cout << item.ullHostTimestamp << ", " << item.ullTimestamp << ", ";
                switch (item.dot11BssType)
                case dot11_BSS_type_infrastructure: std::cout << "infastructure"; break;
                case dot11_BSS_type_independent: std::cout << "independend"; break;
                case dot11_BSS_type_any:std::cout << "any"; break;
                default: std::cout << "";

                std::cout << std::endl;



That’s everything that is needed to scan for WiFi information on Windows. If you would like to see the full source code for a console program that performs these steps, I have it posted on GitHub here.

The information is printed to standard output where it can be viewed. When I need to save it, I direct standard output to a file. Many utilities support this format. I’ve used Excel, Sheets, and SQL Server Bulk Insert for processing this information.

I’m working on an explanation for how to use the same functionality on Android. That will come to this space in a couple of weeks with working code being made available on GitHub.

Mastodon: @j2inet@masto.ai
Instagram: @j2inet
Facebook: @j2inet
YouTube: @j2inet
Telegram: j2inet
Twitter: @j2inet

Posts may contain products with affiliate links. When you make purchases using these links, we receive a small commission at no extra cost to you. Thank you for your support.