주변의 모든 블루투스 기기 스캔하여 MFC 리스트 뷰에 출력
- 최초 작성일: 2023년 3월 31일 (금)
목차
[TOC]
내용
주변의 모든 블루투스 기기를 스캔하고, 해당 기기의 장치명(LocalName)과 SSID(신호 세기) 를 출력하는 프로그램이며,
Acrylic Suite 의 “Acrylic BLE Analyzer” 앱을 사용했을 때의 결과와 비슷한 결과를 출력한다.
이 프로그램을 사용하면, 주변의 블루투스 기기들을 스캔하고 조건에 맞는 기기들을 리스트 박스에 출력할 수 있다.
필요에 따라 기기 이름 필터와 스캔 시간을 조정하여 검색 결과를 개선할 수 있다.
코드 구성
- using namespace winrt::Windows::Devices::Bluetooth::Advertisement; 를 사용하여 블루투스 Advertisement API를 사용할 수 있도록 한다.
- BluetoothLEAdvertisementWatcher watcher; 객체를 생성한다. 이 객체는 블루투스 Low Energy 장치를 검색하는 데 사용된다.
- watcher.Received 이벤트 핸들러를 설정한다. 이 이벤트는 블루투스 LE Advertisement 패킷이 수신될 때마다 호출된다. 여기에서 각 기기의 이름을 가져와 필터링 조건을 확인한다. 조건에 맞는 기기들은 m_bleDevices 맵에 저장되고, 해당 기기의 RSSI 값이 업데이트된다.
- watcher.Stopped 이벤트 핸들러를 설정한다. 이 이벤트는 블루투스 LE Advertisement Watcher가 중지될 때 호출된다.
- watcher.Start(); 를 호출하여 블루투스 LE Advertisement Watcher를 시작한다. 이 시점부터 watcher.Received 이벤트 핸들러가 호출되기 시작한다.
- std::this_thread::sleep_for(std::chrono::seconds(5)); 를 사용하여 스캔 시간을 정의한다. 이 예에서는 5초 동안 스캔한다.
- 스캔 시간이 지나면 watcher.Stop(); 를 호출하여 블루투스 LE Advertisement Watcher를 중지한다.
- m_completed 변수를 사용하여 Watcher가 완전히 중지될 때까지 기다린다.
- 스캔이 완료되면 PostMessage(WM_UPDATE_BLE_LIST, 0, 0); 를 호출하여 메인 스레드에서 리스트 박스를 업데이트하도록 메시지를 보낸다.
참고
만약, 빌드 시 아래와 같은 에러가 발생한다면, Windows SDK Version을 업데이트해주어야 한다.
필자는 10.0.22621.0 버전으로 업데이트해주니 문제없이 빌드되었다.
Severity Code Description Project File Line Suppression State
Error C2039 'wait_for': is not a member of 'winrt::impl' ConsoleApplication2 C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\cppwinrt\winrt\impl\Windows.Foundation.0.h 983
Message see declaration of 'winrt::impl' ConsoleApplication2 C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\cppwinrt\winrt\impl\Windows.Foundation.0.h 103
Message see reference to class template instantiation 'winrt::impl::consume_Windows_Foundation_IAsyncAction<D>' being compiled ConsoleApplication2 C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\cppwinrt\winrt\impl\Windows.Foundation.0.h 985
또한, 다음 헤더 파일들을 include 해야한다.
#include <iostream>
#include <string>
#include <map>
#include <chrono>
#include <thread>
#include <winrt/Windows.Devices.Bluetooth.Advertisement.h>
#include <winrt/Windows.Foundation.h>
또한, 이 코드는 Windows 10 (또는 그 이상)에서만 실행되며, Windows Runtime C++ Template Library (WinRT C++ /20)와 함께 사용되어야 한다.
WinRT를 사용하려면 프로젝트에 필요한 NuGet 패키지를 설치해야 한다.
Visual Studio에서 프로젝트를 클릭한 후 “Manage NuGet Packages”를 선택하고 “Microsoft.Windows.CppWinRT”를 찾아 설치한다.
그런 다음 프로젝트 속성에서 C++ 언어 표준을 “/std:c++17” 또는 그 이상으로 설정해야한다.
이 코드가 클래스의 멤버 함수로 작성되어 있다고 가정하면, 해당 클래스 선언을 헤더 파일에 추가해야 한다.
예를 들면 다음과 같다.
#pragma once
#include <winrt/Windows.Devices.Bluetooth.Advertisement.h>
#include <map>
#include <string>
#include <chrono>
class CMainDlg
{
public:
void ScanForBluetoothLEDevices();
private:
winrt::Windows::Devices::Bluetooth::Advertisement::BluetoothLEAdvertisementWatcher m_watcher;
std::map<std::wstring, int16_t> m_bleDevices;
std::map<std::wstring, std::chrono::steady_clock::time_point> m_bleDevicesLastSeen;
bool m_completed;
static constexpr UINT WM_UPDATE_BLE_LIST = WM_USER + 1; // 사용자 정의 메시지
};
소스 (MFC)
MainDlg.h
public:
CListBox m_list_ble;
std::map<std::wstring, int> m_bleDevices;
std::map<std::wstring, std::chrono::steady_clock::time_point> m_bleDevicesLastSeen;
std::thread bleScanThread;
bool m_completed = false;
winrt::Windows::Devices::Bluetooth::Advertisement::BluetoothLEAdvertisementWatcher m_watcher;
void ScanForBluetoothLEDevices();
afx_msg void OnBnClickedBtnBlescanstop();
MainDlg.cpp
BEGIN_MESSAGE_MAP(CMainDlg, CDialogEx)
ON_MESSAGE(WM_UPDATE_BLE_LIST, &CMainDlg::OnUpdateBLEList)
END_MESSAGE_MAP()
LRESULT CMainDlg::OnUpdateBLEList(WPARAM wParam, LPARAM lParam)
{
// 타임아웃 (예: 3초).
const auto timeout = std::chrono::seconds(3);
// 만료된 기기 제거
for (auto it = m_bleDevicesLastSeen.begin(); it != m_bleDevicesLastSeen.end();)
{
if (std::chrono::steady_clock::now() - it->second > timeout)
{
m_bleDevices.erase(it->first);
it = m_bleDevicesLastSeen.erase(it);
}
else
{
++it;
}
}
// 리스트 박스 리셋.
m_list_ble.ResetContent();
// 맵에 저장된 블루투스 기기를 리스트 박스에 추가
for (const auto& device : m_bleDevices)
{
CString deviceInfo;
std::string localName(device.first.begin(), device.first.end()); // Convert to std::string
deviceInfo.Format(_T("%s - RSSI: %d"), localName.c_str(), device.second);
m_list_ble.AddString(deviceInfo);
}
return 0;
}
void CMainDlg::ScanForBluetoothLEDevices()
{
using namespace winrt::Windows::Devices::Bluetooth::Advertisement;
try
{
BluetoothLEAdvertisementWatcher watcher;
m_completed = false;
// Device found event
m_watcher.Received([&](BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args)
{
std::wstring localName = args.Advertisement().LocalName().c_str();
// 블루투스 기기를 맵에 저장하고 RSSI 값을 업데이트
m_bleDevices[localName] = args.RawSignalStrengthInDBm();
m_bleDevicesLastSeen[localName] = std::chrono::steady_clock::now();
// 메인 스레드에서 리스트 박스를 업데이트하도록 메시지를 보냄
PostMessage(WM_UPDATE_BLE_LIST, 0, 0);
});
// Stopped event
m_watcher.Stopped([&](BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementWatcherStoppedEventArgs args)
{
m_completed = true;
});
// Start the watcher
m_watcher.Start();
// Wait until the watcher stops
while (!m_completed)
{
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
catch (const winrt::hresult_error& ex)
{
std::wcerr << L"Exception thrown: " << ex.message().c_str() << std::endl;
}
}
void CMainDlg::OnBnClickedBtnBlescan()
{
// 기존 스레드가 실행 중이면 종료하고 새로 시작
if (bleScanThread.joinable())
{
bleScanThread.join();
}
bleScanThread = std::thread(&CMainDlg::ScanForBluetoothLEDevices, this);
}
void CMainDlg::OnBnClickedBtnBlescanstop()
{
// 기존 스레드가 실행 중이면 종료
if (bleScanThread.joinable())
{
m_completed = true;
m_watcher.Stop();
bleScanThread.join();
}
}
추가 소스
위의 소스는 쓰레드를 이용하여, 최신 정보를 계속적으로 갱신하는 구조이다.
필요에 의해, 버튼 클릭 이벤트가 있을 때만 해당 시점의 최신 정보를 한 번 갱신하도록 수정해보았다.
또한, 원하는 문자열이 들어간 블루투스 기기만 출력하도록 하였다.
// 블루투스 기기 목록을 리스트 박스에 출력하는 메시지 핸들러
LRESULT CMainDlg::OnUpdateBLEList(WPARAM wParam, LPARAM lParam)
{
// 리스트 박스 (IDC_LIST_BLE_DEVICES: 리스트 박스의 리소스 ID).
//CListBox* pListBox = (CListBox*)GetDlgItem(IDC_LIST_BLE);
//if (pListBox == nullptr)
//{
// return 0;
//}
// 리스트 박스 리셋.
m_list_ble.ResetContent();
// 맵에 저장된 블루투스 기기를 리스트 박스에 추가
for (const auto& device : m_bleDevices)
{
CString deviceInfo;
std::string localName(device.first.begin(), device.first.end()); // Convert to std::string
deviceInfo.Format(_T("%s - RSSI: %d"), localName.c_str(), device.second);
m_list_ble.AddString(deviceInfo);
}
return 0;
}
void CMainDlg::ScanForBluetoothLEDevices()
{
using namespace winrt::Windows::Devices::Bluetooth::Advertisement;
try
{
BluetoothLEAdvertisementWatcher watcher;
m_completed = false;
// Device found event
watcher.Received([&](BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args)
{
std::wstring localName = args.Advertisement().LocalName().c_str();
if (localName.find(L"BLE") == 0)
{
// 블루투스 기기를 맵에 저장하고 RSSI 값을 업데이트
m_bleDevices[localName] = args.RawSignalStrengthInDBm();
m_bleDevicesLastSeen[localName] = std::chrono::steady_clock::now();
}
});
// Stopped event
watcher.Stopped([&](BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementWatcherStoppedEventArgs args)
{
m_completed = true;
});
// Start the watcher
watcher.Start();
// Wait for a short period of time (e.g., 2 seconds) to allow the watcher to scan devices
std::this_thread::sleep_for(std::chrono::seconds(2));
// Stop the watcher
watcher.Stop();
// Wait until the watcher stops
while (!m_completed)
{
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
// 메인 스레드에서 리스트 박스를 업데이트하도록 메시지를 보냄
PostMessage(WM_UPDATE_BLE_LIST, 0, 0);
}
catch (const winrt::hresult_error& ex)
{
std::wcerr << L"Exception thrown: " << ex.message().c_str() << std::endl;
}
}
// "BLE SCAN" Button
void CMainDlg::OnBnClickedBtnBlescan()
{
ScanForBluetoothLEDevices();
}
소스 (C++)
#include <winrt/Windows.Devices.Bluetooth.Advertisement.h>
#include <winrt/Windows.Foundation.h>
#include <iostream>
#include <string>
#include <map>
#include <thread>
#include <chrono>
class BluetoothLEScanner
{
public:
void ScanForBluetoothLEDevices()
{
using namespace winrt::Windows::Devices::Bluetooth::Advertisement;
try
{
BluetoothLEAdvertisementWatcher watcher;
m_completed = false;
// Device found event
watcher.Received([&](BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args)
{
std::wstring localName = args.Advertisement().LocalName().c_str();
if (localName.find(L"BLE") == 0)
{
// Save the Bluetooth device in the map and update the RSSI value
m_bleDevices[localName] = args.RawSignalStrengthInDBm();
m_bleDevicesLastSeen[localName] = std::chrono::steady_clock::now();
}
});
// Stopped event
watcher.Stopped([&](BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementWatcherStoppedEventArgs args)
{
m_completed = true;
});
// Start the watcher
watcher.Start();
// Wait for a short period of time (e.g., 2 seconds) to allow the watcher to scan devices
std::this_thread::sleep_for(std::chrono::seconds(2));
// Stop the watcher
watcher.Stop();
// Wait until the watcher stops
while (!m_completed)
{
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
// Update the list of devices
UpdateDeviceList();
}
catch (const winrt::hresult_error& ex)
{
std::wcerr << L"Exception thrown: " << ex.message().c_str() << std::endl;
}
}
private:
void UpdateDeviceList()
{
for (const auto& device : m_bleDevices)
{
std::wcout << L"Device: " << device.first << L" RSSI: " << device.second << std::endl;
}
}
bool m_completed;
std::map<std::wstring, int16_t> m_bleDevices;
std::map<std::wstring, std::chrono::steady_clock::time_point> m_bleDevicesLastSeen;
};
int main()
{
winrt::init_apartment();
BluetoothLEScanner scanner;
scanner.ScanForBluetoothLEDevices();
return 0;
}