블루투스 저에너지 개요
Qt Bluetooth 저에너지(Bluetooth Low Energy) API는 페리페럴/서버 및 센트럴/클라이언트 역할을 지원합니다. 이 기능은 모든 주요 Qt 플랫폼에서 지원됩니다. 유일한 예외는 Windows에서 페리페럴 역할이 지원되지 않는다는 점입니다.
블루투스 저에너지란?
블루투스 저에너지(Bluetooth Low Energy)는 블루투스 스마트(Bluetooth Smart)라고도 불리며, 2011년에 공식적으로 도입된 무선 컴퓨터 네트워크 기술입니다. 이 기술은 “기존” 블루투스와 동일한 2.4 GHz 주파수 대역에서 작동합니다. 가장 큰 차이점은 기술 이름에서 알 수 있듯이 낮은 에너지 소비량입니다. 덕분에 블루투스 저에너지 기기는 코인형 건전지로 수개월, 심지어 수년 동안 작동할 수 있습니다. 이 기술은 블루투스 v4.0을 통해 도입되었습니다. 이 기술을 지원하는 기기를 ‘블루투스 스마트 레디(Bluetooth Smart Ready) 기기’라고 합니다. 이 기술의 주요 특징은 다음과 같습니다:
- 초저전력 피크, 평균 및 유휴 모드 전력 소비
- 일반 코인형 배터리로 수년 동안 작동 가능
- 저비용
- 다중 벤더 간 상호 운용성
- 향상된 통신 범위
블루투스 저에너지(Bluetooth Low Energy)는 클라이언트-서버 아키텍처를 사용합니다. 서버(주변기기라고도 함)는 온도나 심박수 등의 서비스를 제공하고 이를 브로드캐스팅합니다. 클라이언트(중앙 장치라고도 함)는 서버에 연결하여 서버가 브로드캐스팅한 값을 읽어옵니다. 예를 들어, 온도 조절기, 습도 센서, 압력 센서와 같은 블루투스 스마트 레디(Bluetooth Smart Ready) 센서가 설치된 아파트가 있습니다. 이러한 센서들은 아파트의 환경 값을 알리는 주변기기입니다. 동시에 휴대폰이나 컴퓨터가 해당 센서에 연결하여 값을 가져오고, 이를 더 큰 환경 제어 애플리케이션의 일부로 사용자에게 표시할 수 있습니다.
기본 서비스 구조
블루투스 저에너지(Bluetooth Low Energy)는 ATT(속성 프로토콜, Attribute Protocol)와 GATT(일반 속성 프로파일, Generic Attribute Profile)라는 두 가지 프로토콜을 기반으로 합니다. 이 프로토콜들은 모든 블루투스 스마트 레디(Bluetooth Smart Ready) 기기가 사용하는 통신 계층을 정의합니다.
ATT 프로토콜
ATT의 기본 구성 요소는 속성(attribute)입니다. 각 속성은 다음 세 가지 요소로 구성됩니다:
- 값 - 페이로드 또는 필요한 정보
- UUID - 속성의 유형(GATT에서 사용)
- 16비트 핸들 - 속성의 고유 식별자
서버는 속성을 저장하며, 클라이언트는 ATT 프로토콜을 사용하여 서버의 값을 읽고 씁니다.
GATT 프로파일
GATT는 미리 정의된 UUID에 의미를 부여하여 일련의 속성을 그룹화합니다. 아래 표는 특정 날짜의 심박수를 노출하는 서비스의 예를 보여줍니다. 실제 값은 다음 두 특성 내에 저장됩니다:
| 핸들 | UUID | 값 | 설명 |
|---|---|---|---|
| 0x0001 | 0x2800 | UUID 0x180D | 심박수 서비스 시작 |
| 0x0002 | 0x2803 | UUID 0x2A37, 값 핸들: 0x0003 | 심박수 측정(HRM) 유형의 특성 |
| 0x0003 | 0x2A37 | 65 bpm | 심박수 값 |
| 0x0004 | 0x2803 | UUID 0x2A08, 값 핸들: 0x0005 | Date Time 유형의 특성 |
| 0x0005 | 0x2A08 | 2014년 8월 18일 11:00 | 측정 날짜 및 시간 |
| 0x0006 | 0x2800 | UUID xxxxxx | 다음 서비스 시작 |
| ... | ... | ... | ... |
GATT는 위에서 사용된 UUID 0x2800 가 서비스 정의의 시작을 나타낸다고 명시하고 있습니다. 0x2800 에 뒤따르는 모든 속성은 다음 0x2800 가 나타나거나 끝이 발견될 때까지 해당 서비스의 일부입니다. 이와 유사하게, 잘 알려진 UUID 0x2803 는 특성이 존재함을 나타내며, 각 특성은 값의 성질을 정의하는 유형을 갖습니다. 위의 예시에서는 UUID 0x2A08 (날짜 및 시간)과 0x2A37 (심박수 측정)을 사용합니다. 위의 각 UUID는 Bluetooth Special Interest Group에서 정의한 것으로, GATT 사양에서 확인할 수 있습니다. 가능한 경우 미리 정의된 UUID를 사용하는 것이 바람직하지만, 특성 및 서비스 유형에 대해 새롭고 아직 사용되지 않은 UUID를 사용하는 것도 전적으로 가능합니다.
일반적으로 각 서비스는 하나 이상의 특성(characteristic)으로 구성될 수 있습니다. 특성은 데이터를 포함하며, 추가 정보나 특성을 조작하는 방법을 제공하는 설명자(descriptor)를 통해 더 자세히 설명될 수 있습니다. 모든 서비스, 특성 및 디스크립터는 128비트 UUID로 식별됩니다. 마지막으로, 서비스 내부에 다른 서비스를 포함시키는 것도 가능합니다(아래 그림 참조).

Qt Bluetooth 저전력(Low Energy) API 사용
이 섹션에서는 Qt에서 제공하는 Bluetooth Low Energy API 사용 방법을 설명합니다. 클라이언트 측에서 이 API를 사용하면 주변 기기와 연결을 설정하고, 해당 기기의 서비스를 탐색하며, 기기에 저장된 데이터를 읽고 쓸 수 있습니다. 서버 측에서는 서비스를 설정하고, 서비스를 알리며, 클라이언트가 특성을 기록할 때 알림을 받을 수 있습니다. 아래 예제 코드는 ‘심박수 게임(Heart Rate Game)’ 및 ‘심박수 서버(Heart Rate Server )’ 예제에서 발췌한 것입니다.
연결 설정
블루투스 저에너지 주변 기기의 특성(characteristics)을 읽고 쓰려면, 해당 기기를 찾아 연결해야 합니다. 이를 위해서는 주변 기기가 자신의 존재와 서비스를 알리는(advertise) 것이 필요합니다. QBluetoothDeviceDiscoveryAgent 클래스를 사용하여 기기 탐색을 시작합니다. 이 클래스의 QBluetoothDeviceDiscoveryAgent::deviceDiscovered() 시그널에 연결하고, start()을 호출하여 검색을 시작합니다:
m_deviceDiscoveryAgent = new QBluetoothDeviceDiscoveryAgent(this);
m_deviceDiscoveryAgent->setLowEnergyDiscoveryTimeout(15000);
connect(m_deviceDiscoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered,
this, &DeviceFinder::addDevice);
connect(m_deviceDiscoveryAgent, &QBluetoothDeviceDiscoveryAgent::errorOccurred,
this, &DeviceFinder::scanError);
connect(m_deviceDiscoveryAgent, &QBluetoothDeviceDiscoveryAgent::finished,
this, &DeviceFinder::scanFinished);
connect(m_deviceDiscoveryAgent, &QBluetoothDeviceDiscoveryAgent::canceled,
this, &DeviceFinder::scanFinished);
m_deviceDiscoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);저희는 저에너지(Low Energy) 장치에만 관심이 있으므로, 수신 슬롯 내에서 장치 유형을 필터링합니다. 장치 유형은 ` QBluetoothDeviceInfo::coreConfigurations()` 플래그를 사용하여 확인할 수 있습니다. ` deviceDiscovered()` 신호는 더 자세한 정보가 발견됨에 따라 동일한 장치에 대해 여러 번 발신될 수 있습니다. 여기서는 사용자가 개별 장치만 볼 수 있도록 이러한 장치 발견 정보를 매칭합니다:
void DeviceFinder::addDevice(const QBluetoothDeviceInfo &device)
{
// If device is LowEnergy-device, add it to the list
if (device.coreConfigurations() & QBluetoothDeviceInfo::LowEnergyCoreConfiguration) {
auto devInfo = new DeviceInfo(device);
auto it = std::find_if(m_devices.begin(), m_devices.end(),
[devInfo](DeviceInfo *dev) {
return devInfo->getAddress() == dev->getAddress();
});
if (it == m_devices.end()) {
m_devices.append(devInfo);
} else {
auto oldDev = *it;
*it = devInfo;
delete oldDev;
}
setInfo(tr("Low Energy device found. Scanning more..."));
setIcon(IconProgress);
}
//...
}주변 기기의 주소를 파악하면 QLowEnergyController 클래스를 사용합니다. 이 클래스는 모든 Bluetooth Low Energy 개발의 진입점입니다. 이 클래스의 생성자는 원격 기기의 QBluetoothAddress 를 인수로 받습니다. 마지막으로 일반적인 슬롯을 설정하고 connectToDevice()를 사용하여 기기에 직접 연결합니다:
m_control = QLowEnergyController::createCentral(m_currentDevice->getDevice(), this);
connect(m_control, &QLowEnergyController::serviceDiscovered,
this, &DeviceHandler::serviceDiscovered);
connect(m_control, &QLowEnergyController::discoveryFinished,
this, &DeviceHandler::serviceScanDone);
connect(m_control, &QLowEnergyController::errorOccurred, this,
[this](QLowEnergyController::Error error) {
Q_UNUSED(error);
setError("Cannot connect to remote device.");
setIcon(IconError);
});
connect(m_control, &QLowEnergyController::connected, this, [this]() {
setInfo("Controller connected. Search services...");
setIcon(IconProgress);
m_control->discoverServices();
});
connect(m_control, &QLowEnergyController::disconnected, this, [this]() {
setError("LowEnergy controller disconnected");
setIcon(IconError);
});
// Connect
m_control->connectToDevice();서비스 검색
위의 코드 조각은 연결이 설정된 후 애플리케이션이 서비스 검색을 시작하는 방법을 보여줍니다.
아래의 ` serviceDiscovered() ` 슬롯은 ` QLowEnergyController::serviceDiscovered()` 신호에 의해 트리거되며, 간헐적인 진행 상황 보고를 제공합니다. 주변의 HeartRate 기기를 모니터링하는 하트 리스너 앱에 관한 것이므로, ` QBluetoothUuid::ServiceClassUuid::HeartRate` 유형이 아닌 서비스는 모두 무시합니다.
void DeviceHandler::serviceDiscovered(const QBluetoothUuid &gatt)
{
if (gatt == QBluetoothUuid(QBluetoothUuid::ServiceClassUuid::HeartRate)) {
setInfo("Heart Rate service discovered. Waiting for service scan to be done...");
setIcon(IconProgress);
m_foundHeartRateService = true;
}
}최종적으로 서비스 검색이 성공적으로 완료되었음을 나타내기 위해 QLowEnergyController::discoveryFinished() 신호가 발송됩니다. HeartRate 서비스가 발견된 경우, 해당 서비스를 나타내는 QLowEnergyService 인스턴스가 생성됩니다. 반환된 서비스 객체는 업데이트 알림에 필요한 신호를 제공하며, QLowEnergyService::discoverDetails()을 사용하여 서비스 세부 정보 검색이 트리거됩니다:
// If heartRateService found, create new service
if (m_foundHeartRateService)
m_service = m_control->createServiceObject(QBluetoothUuid(QBluetoothUuid::ServiceClassUuid::HeartRate), this);
if (m_service) {
connect(m_service, &QLowEnergyService::stateChanged, this, &DeviceHandler::serviceStateChanged);
connect(m_service, &QLowEnergyService::characteristicChanged, this, &DeviceHandler::updateHeartRateValue);
connect(m_service, &QLowEnergyService::descriptorWritten, this, &DeviceHandler::confirmedDescriptorWrite);
m_service->discoverDetails();
} else {
setError("Heart Rate Service not found.");
setIcon(IconError);
}상세 검색 과정에서 서비스의 ` state()`는 ` RemoteService `에서 ` RemoteServiceDiscovering `로 전환되며, 최종적으로 ` RemoteServiceDiscovered`으로 종료됩니다:
void DeviceHandler::serviceStateChanged(QLowEnergyService::ServiceState s)
{
switch (s) {
case QLowEnergyService::RemoteServiceDiscovering:
setInfo(tr("Discovering services..."));
setIcon(IconProgress);
break;
case QLowEnergyService::RemoteServiceDiscovered:
{
setInfo(tr("Service discovered."));
setIcon(IconBluetooth);
const QLowEnergyCharacteristic hrChar =
m_service->characteristic(QBluetoothUuid(QBluetoothUuid::CharacteristicType::HeartRateMeasurement));
if (!hrChar.isValid()) {
setError("HR Data not found.");
setIcon(IconError);
break;
}
m_notificationDesc = hrChar.descriptor(QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration);
if (m_notificationDesc.isValid())
m_service->writeDescriptor(m_notificationDesc, QByteArray::fromHex("0100"));
break;
}
default:
//nothing for now
break;
}
emit aliveChanged();
}주변 장치와의 상호작용
위의 코드 예제에서 원하는 특성(characteristic)은 HeartRateMeasurement 유형입니다. 애플리케이션이 심박수 변화를 측정하므로, 해당 특성에 대한 변경 알림을 활성화해야 합니다. 모든 특성이 변경 알림을 제공하는 것은 아니라는 점에 유의하십시오. HeartRate 특성은 표준화되었으므로 알림을 수신할 수 있다고 가정할 수 있습니다. 궁극적으로 적절한 알림의 가용성을 확인하려면 QLowEnergyCharacteristic::properties()에 QLowEnergyCharacteristic::Notify 플래그가 설정되어 있어야 하며, QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration 유형의 디스크립터가 존재해야 합니다.
마지막으로, Bluetooth Low Energy 표준에 따라 HeartRate 특성 값을 처리합니다:
void DeviceHandler::updateHeartRateValue(const QLowEnergyCharacteristic &c, const QByteArray &value)
{
// ignore any other characteristic change -> shouldn't really happen though
if (c.uuid() != QBluetoothUuid(QBluetoothUuid::CharacteristicType::HeartRateMeasurement))
return;
auto data = reinterpret_cast<const quint8 *>(value.constData());
quint8 flags = *data;
//Heart Rate
int hrvalue = 0;
if (flags & 0x1) // HR 16 bit? otherwise 8 bit
hrvalue = static_cast<int>(qFromLittleEndian<quint16>(data[1]));
else
hrvalue = static_cast<int>(data[1]);
addMeasurement(hrvalue);
}일반적으로 특성 값은 일련의 바이트로 구성됩니다. 이러한 바이트의 정확한 해석은 특성 유형과 값 구조에 따라 달라집니다. 상당수는 Bluetooth SIG에 의해 표준화되었지만, 일부는 사용자 정의 프로토콜을 따를 수도 있습니다. 위의 코드 스니펫은 표준화된 HeartRate 값을 읽는 방법을 보여줍니다.
광고 서비스
주변 기기에서 GATT 서버 애플리케이션을 구현하는 경우, 중앙 기기에 제공하고자 하는 서비스를 정의하고 이를 광고해야 합니다:
QLowEnergyAdvertisingData advertisingData;
advertisingData.setDiscoverability(QLowEnergyAdvertisingData::DiscoverabilityGeneral);
advertisingData.setIncludePowerLevel(true);
advertisingData.setLocalName("HeartRateServer");
advertisingData.setServices(QList<QBluetoothUuid>() << QBluetoothUuid::ServiceClassUuid::HeartRate);
bool errorOccurred = false;
const std::unique_ptr<QLowEnergyController> leController(QLowEnergyController::createPeripheral());
auto errorHandler = [&leController, &errorOccurred](QLowEnergyController::Error errorCode) {
qWarning().noquote().nospace() << errorCode << " occurred: "
<< leController->errorString();
if (errorCode != QLowEnergyController::RemoteHostClosedError) {
qWarning("Heartrate-server quitting due to the error.");
errorOccurred = true;
QCoreApplication::quit();
}
};
QObject::connect(leController.get(), &QLowEnergyController::errorOccurred, errorHandler);
std::unique_ptr<QLowEnergyService> service(leController->addService(serviceData));
leController->startAdvertising(QLowEnergyAdvertisingParameters(), advertisingData,
advertisingData);
if (errorOccurred)
return-1;이제 잠재적인 클라이언트들은 우리 기기에 연결하여 제공되는 서비스를 탐색하고, 특성 값의 변경 사항을 알림받기 위해 등록할 수 있습니다. API의 이 부분은 앞서 설명한 섹션에서 이미 다루었습니다.
주변 기기에서 서비스 구현하기
첫 번째 단계는 서비스와 그 특성 및 디스크립터를 정의하는 것입니다. 이는 QLowEnergyServiceData, QLowEnergyCharacteristicData 및 QLowEnergyDescriptorData 클래스를 사용하여 수행됩니다. 이 클래스들은 정의될 블루투스 저에너지(BLE) 서비스를 구성하는 필수 정보를 담는 컨테이너 또는 구성 요소 역할을 합니다. 아래 코드 스니펫은 측정된 분당 심박수를 게시하는 간단한 HeartRate 서비스를 정의합니다. 이러한 서비스가 활용될 수 있는 예로는 손목시계가 있습니다.
QLowEnergyCharacteristicData charData;
charData.setUuid(QBluetoothUuid::CharacteristicType::HeartRateMeasurement);
charData.setValue(QByteArray(2, 0));
charData.setProperties(QLowEnergyCharacteristic::Notify);
const QLowEnergyDescriptorData clientConfig(QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration,
QByteArray(2, 0));
charData.addDescriptor(clientConfig);
QLowEnergyServiceData serviceData;
serviceData.setType(QLowEnergyServiceData::ServiceTypePrimary);
serviceData.setUuid(QBluetoothUuid::ServiceClassUuid::HeartRate);
serviceData.addCharacteristic(charData);결과로 생성된 ` serviceData ` 객체는 위의 ‘서비스 광고(Advertising Services )’ 섹션에 설명된 대로 전송될 수 있습니다. ` QLowEnergyServiceData `와 ` QLowEnergyAdvertisingData `가 감싸고 있는 정보 간에 부분적으로 중복되는 부분이 있음에도 불구하고, 이 두 클래스는 매우 다른 두 가지 역할을 수행합니다. 광고 데이터는 주변 기기로 전송되며, 29바이트라는 크기 제한으로 인해 범위가 종종 제한됩니다. 따라서 이 데이터가 항상 100% 완전한 것은 아닙니다. 이에 비해 QLowEnergyServiceData 내에 포함된 서비스 데이터는 완전한 서비스 데이터 세트를 제공하며, 활성 서비스 검색을 통해 연결이 수행된 경우에만 연결된 클라이언트에 표시됩니다.
다음 섹션에서는 서비스가 심박수 값을 업데이트하는 방법을 보여줍니다. 서비스의 특성에 따라 https://www.bluetooth.org에 정의된 공식 서비스 정의를 준수해야 할 수도 있습니다. 다른 서비스는 완전히 사용자 정의될 수 있습니다. 심박수 서비스가 채택되었으며, 해당 사양은 https://www.bluetooth.com/specifications/adopted-specifications에서 확인할 수 있습니다.
QTimer heartbeatTimer;
quint8 currentHeartRate = 60;
enum ValueChange { ValueUp, ValueDown } valueChange = ValueUp;
const auto heartbeatProvider = [&service, ¤tHeartRate, &valueChange]() {
QByteArray value;
value.append(char(0)); // Flags that specify the format of the value.
value.append(char(currentHeartRate)); // Actual value.
QLowEnergyCharacteristic characteristic
= service->characteristic(QBluetoothUuid::CharacteristicType::HeartRateMeasurement);
Q_ASSERT(characteristic.isValid());
service->writeCharacteristic(characteristic, value); // Potentially causes notification.
if (currentHeartRate == 60)
valueChange = ValueUp;
else if (currentHeartRate == 100)
valueChange = ValueDown;
if (valueChange == ValueUp)
++currentHeartRate;
else
--currentHeartRate;
};
QObject::connect(&heartbeatTimer, &QTimer::timeout, heartbeatProvider);
heartbeatTimer.start(1000);일반적으로 주변 기기의 특성 및 설명자 값을 업데이트할 때는 블루투스 저에너지(Bluetooth Low Energy) 기기를 연결할 때와 동일한 방법을 사용합니다.
참고: iOS에서 Qt Bluetooth (센트럴 및 페리페럴 역할 모두에서)를 사용하려면 사용 설명이 포함된 Info.plist 파일을 제공해야 합니다. CoreBluetooth 문서에 따르면, 앱이 액세스해야 하는 데이터 유형에 대한 사용 설명 키가 Info.plist에 포함되어 있지 않으면 앱이 충돌합니다. iOS 13 이상에서 링크된 앱에서 Core Bluetooth API에 액세스하려면 NSBluetoothAlwaysUsageDescription 키를 포함해야 합니다. iOS 12 및 그 이전 버전에서는 Bluetooth 주변기기 데이터에 액세스하기 위해 NSBluetoothPeripheralUsageDescription을 포함해야 합니다.
© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.