본문 바로가기

개발 일지

[C#] .NET 프레임워크로 윈도우에서 Bluetooth 사용하기 (2)

이전 포스트에서 C# Console Application 에서 BLE 를 사용할 수 있도록 설정하고, 주변의 디바이스를 쿼리하는 부분까지 다뤄보았다. 이번 포스트에서는 실제로 Device 의 characteristic 으로부터 데이터를 읽어오는 작업을 진행해보겠다.

 

[C#] .NET 프레임워크로 윈도우에서 Bluetooth 사용하기

데스크탑에서 BLE 디바이스와 소통하는 앱을 제작해야 하는 태스크를 맡게 되어 Windows 플랫폼에서 구동하는 데스크탑 앱을 제작하기로 결정하였다. Cross-platform 으로 동작하는 bleak 이라는 패키지

dev-seb.tistory.com

GATT 서버의 구조

원하는 BLE 디바이스를 검색했으면, 디바이스와 통신하기 위해서 두 가지의 개념을 사용한다. Service 와 Characteristic 이 그것이다.

GATT

[출처] Performance Evaluation of Bluetooth Low Energy: A Systematic Review - Scientific Figure on ResearchGate

 

디바이스로부터 Service 를 찾고, 그 Service 안의 Characteristic 을 찾아서 데이터를 읽어오는 작업을 수행하면 된다.

BLE 디바이스와 연결

이전 포스트에서 Bluetooth Device 를 검색했던 코드를 BluetoothLE Device 를 검색하도록 변경하자.

var watcher = DeviceInformation.CreateWatcher(
    BluetoothDevice.GetDeviceSelectorFromPairingState(false)
);

 

위의 코드에서 Device Selector를 변경하여

var watcher = DeviceInformation.CreateWatcher(
    BluetoothLEDevice.GetDeviceSelectorFromPairingState(false)
);

 

로 변경한다. 코드를 실행해 보면, 기존과 달리, BLE 디바이스가 검색되는 것을 확인할 수 있다.

 

공식문서 에 따르면, BluetoothLEDevice.FromIdAsync 를 이용해서 디바이스와 연결할 수 있는데, 이 함수를 호출한다고 해서 바로 연결이 이루어지는 것은 아니고, 서비스 검색 혹은 특성 검색을 실행해야 연결이 이루어진다고 한다.

이전 포스트에서 작성한 코드를 살펴보면, Added 이벤트 콜백에서 DeviceInformation 을 인자로 받아오는 것을 확인할 수 있다.

void Watcher_Added(DeviceWatcher sender, DeviceInformation info)
{
    Console.WriteLine($"Found {info.Name}: {info.Id}");
}

 

DeviceInformation 의 Id 를 이용해서 Device 와 연결하는 코드를 Watcher_Added 안에 넣도록 해보자. 편의를 위해서, 검색되는 모든 디바이스가 아니라, 최초로 연결되는 디바이스 1대에 대해서만 연결을 하도록 설정하였다. 추가적으로, FromIdAsync 함수를 사용하기 위해, Watcher_Added 함수를 async 함수로 변경한다.

BluetoothLEDevice? device = null;

async void Watcher_Added(DeviceWatcher sender, DeviceInformation info) // async 로 변경됨
{
    Console.WriteLine($"Found {info.Name} {info.Id}");
    if(device == null)
    {
        watcher.Stop();
        device = await BluetoothLEDevice.FromIdAsync(info.Id);
    }
}

 

첫 번째 디바이스가 검색되면, 검색을 멈추고, 찾은 디바이스와 연결을 시작한다. 위에서 기술한 대로, 서비스 검색을 시작하지 않는 이상 연결을 시작하지 않으므로, 검색된 디바이스의 서비스를 열거하는 함수를 만든다.

using Windows.Devices.Bluetooth.GenericAttributeProfile;

async Task ListServices(BluetoothLEDevice device)
{
    GattDeviceServicesResult result = await device.GetGattServicesAsync(); // 서비스 검색
    if(result.Status == GattCommunicationStatus.Success) // 성공 시에
    {
        var services = result.Services;
        foreach(var s in services) // 서비스 열거
        {
            Console.WriteLine($"Service {s.Uuid}");
        }
    } else
    {
        Console.WriteLine("Failed to get services");
    }
}

 

서비스를 검색했으면, 각각의 서비스에서 특성을 검색 후 열거할 수 있다. 서비스를 받아서 특성을 열거하는 함수를 작성한다.

async Task ListCharacteristics(GattDeviceService service)
{
    GattCharacteristicsResult result = await service.GetCharacteristicsAsync(); // 특성 검색
    if (result.Status == GattCommunicationStatus.Success)
    {
        var characteristics = result.Characteristics;
        foreach (var c in characteristics) // 특성 열거
        {
            Console.WriteLine($"> Characteristic {c.Uuid}");
        }
    }
    else
    {
        Console.WriteLine("Failed to get characteristics");
    }
}

 

인하우스로 개발된 BLE 디바이스의 경우에는, 타겟으로 하는 서비스와 특성의 UUID 값을 알고 있는 경우가 많다. 만약 타겟으로 하는 서비스와 특성이 있다면, BluetoothLEDevice.GetGattServicesForUuidAsync(Guid) 혹은 GattDeviceService.GetCharacteristicsForUuidAsync(Guid) 를 사용하면 된다.

Guid serviceUUID = Guid.Parse("********-****-****-****-************");
Guid characteristicUUID = Guid.Parse("********-****-****-****-************");

await device.GetGattServicesForUuidAsync(serviceUUID);
await service.GetCharacteristicsForUuidAsync(characteristicUUID);

 

ListServices 함수에서 검색된 서비스의 특성을 열거하도록 코드 한 줄을 추가해준다.

using Windows.Devices.Bluetooth.GenericAttributeProfile;

async Task ListServices(BluetoothLEDevice device)
{
    GattDeviceServicesResult result = await device.GetGattServicesAsync();
    if(result.Status == GattCommunicationStatus.Success)
    {
        var services = result.Services;
        foreach(var s in services)
        {
            Console.WriteLine($"Service {s.Uuid}");
            await ListCharacteristics(s); // 추가됨
        }
    } else
    {
        Console.WriteLine("Failed to get services");
    }
}

 

코드를 실행하면, 다음과 같이 첫번째로 검색된 디바이스에 대해 서비스와 특성을 열거한다.

서비스와 특성 열거

 

특성에서 데이터 읽기

공식문서에 데이터를 읽는 방법이 기술되어 있다. 해당 코드를 참고해서, 특성이 읽기를 지원하는 경우 데이터를 읽어오는 함수를 작성한다.

using Windows.Storage.Streams;

async Task ReadIfApplicable(GattCharacteristic characteristic) {
    var properties = characteristic.CharacteristicProperties;
    if(properties.HasFlag(GattCharacteristicProperties.Read)) // 읽기가 지원되는 경우
    {
        GattReadResult result = await characteristic.ReadValueAsync(); // 데이터를 읽는다.
        if(result.Status == GattCommunicationStatus.Success)
        {
            // 데이터를 byte array 로 읽는다.
            var reader = DataReader.FromBuffer(result.Value);
            byte[] data = new byte[reader.UnconsumedBufferLength];
            reader.ReadBytes(data);
            Console.WriteLine($"Read {data.Length} bytes");
        }
        else
        {
            Console.WriteLine($"Failed to read from {characteristic.Uuid}");
        }
    }
    else
    {
        Console.WriteLine($"Cannot read from {characteristic.Uuid}");
    }
}

 

검색된 특성에서 데이터를 읽어오도록 한 줄의 코드를 추가한다.

async Task ListCharacteristics(GattDeviceService service)
{
    GattCharacteristicsResult result = await service.GetCharacteristicsAsync();
    if (result.Status == GattCommunicationStatus.Success)
    {
        var characteristics = result.Characteristics;
        foreach (var c in characteristics)
        {
            Console.WriteLine($"> Characteristic {c.Uuid}");
            await ReadIfApplicable(c); // 추가됨
        }
    }
    else
    {
        Console.WriteLine("Failed to get characteristics");
    }
}

 

실행해 보면, 다음과 같이 데이터를 읽어오는 것을 확인할 수 있다.

데이터를 잘 읽어온다

결론

IoT 기술에 대한 관심이 높아지면서, BLE 를 이용한 inter-device 통신 역시도 빈번하게 사용되는 것 같다. 읽어온 이후의 데이터에 대해서는 protocol 에 따라 해석해야 하기 때문에, 이 데모에서는 시연하기가 제한되지만, byte array 로 읽어온 이후의 해석은 straight forward 할 것이다.

 

전체 코드

using Windows.Devices.Bluetooth;
using Windows.Devices.Bluetooth.GenericAttributeProfile;
using Windows.Devices.Enumeration;
using Windows.Storage.Streams;


BluetoothLEDevice? device = null;

var watcher = DeviceInformation.CreateWatcher(
    BluetoothLEDevice.GetDeviceSelectorFromPairingState(false)
);

watcher.Added += Watcher_Added;
watcher.Stopped += Watcher_Stopped;


async void Watcher_Added(DeviceWatcher sender, DeviceInformation info)
{
    Console.WriteLine($"Found {info.Name}: {info.Id}");
    if(device == null)
    {
        watcher.Stop();
        device = await BluetoothLEDevice.FromIdAsync(info.Id);
        await ListServices(device);
    }
}

async Task ListServices(BluetoothLEDevice device)
{
    GattDeviceServicesResult result = await device.GetGattServicesAsync();
    if(result.Status == GattCommunicationStatus.Success)
    {
        var services = result.Services;
        foreach(var s in services)
        {
            Console.WriteLine($"Service ${s.Uuid}");
            await ListCharacteristics(s);
        }
    } else
    {
        Console.WriteLine("Failed to get services");
    }
}

async Task ListCharacteristics(GattDeviceService service)
{
    GattCharacteristicsResult result = await service.GetCharacteristicsAsync();
    if (result.Status == GattCommunicationStatus.Success)
    {
        var characteristics = result.Characteristics;
        foreach (var c in characteristics)
        {
            Console.WriteLine($"> Characteristic {c.Uuid}");
            await ReadIfApplicable(c);
        }
    }
    else
    {
        Console.WriteLine("Failed to get characteristics");
    }
}

async Task ReadIfApplicable(GattCharacteristic characteristic) {
    var properties = characteristic.CharacteristicProperties;
    if(properties.HasFlag(GattCharacteristicProperties.Read))
    {
        GattReadResult result = await characteristic.ReadValueAsync();
        if(result.Status == GattCommunicationStatus.Success)
        {
            var reader = DataReader.FromBuffer(result.Value);
            byte[] data = new byte[reader.UnconsumedBufferLength];
            reader.ReadBytes(data);
            Console.WriteLine($"Read {data.Length} bytes");
        }
        else
        {
            Console.WriteLine($"Failed to read from {characteristic.Uuid}");
        }
    }
    else
    {
        Console.WriteLine($"Cannot read from {characteristic.Uuid}");
    }
}

void Watcher_Stopped(DeviceWatcher sender, object args)
{
    Console.WriteLine("Watcher Stopped");
}

watcher.Start();

Console.WriteLine("Press ENTER to quit");
Console.ReadLine();

device?.Dispose();

 

 

GitHub - k2sebeom/csharp-ble-demo: Demonstration of reading BLE using .NET framework and C#

Demonstration of reading BLE using .NET framework and C# - GitHub - k2sebeom/csharp-ble-demo: Demonstration of reading BLE using .NET framework and C#

github.com