강좌 전체보기

.

이번 강좌부터는 본격적으로 BLE 장치들을 만들어서 구동시켜 보겠습니다. 가장 먼저 해볼 실습은 비컨(Beacon) 장치를 만들고 스캔하는 방법입니다.

앞선 파트에서 설명했듯이, 비컨 장치는 advertising 할 때 사용자가 넣을 수 있는 데이터 공간(31 byte)에 소량의 정보를 담아 주변에 뿌리는 일을 하도록 설계된 장치입니다. 그리고 31 byte 사용자 지정 가능한 데이터 공간을 어떻게 나누어 사용할지 여러 표준이 있는데, 여기서는 가장 유명한 iBeacon 을 이용합니다.

iBeacon 장치를 만들기 위해서는 필연적으로 iBeacon의 스펙을 이해할 필요가 있습니다.

.

iBeacon spec

iBeacon으로 동작하든, 일반 BLE 장치로 동작하든 advertising packet 은 아래와 같은 구조를 갖습니다. iBeacon 장치는 여기서 Advertiser’s Data 를 사용하는 방법이 틀릴 뿐입니다.

Advertiser’s Data (Max 31 byte) 영역은 iBeacon prefix, UUID, Major, Minor, TX Power 로 나뉩니다.

  • iBeacon prefix
    • 비컨의 설정이나 특성값이 기록되는 부분입니다. iBeacon 헤더 정보라 생각하시면 됩니다. 우리가 손대지 않고 정해진 값을 사용해도 됩니다.
  • UUID (16 byte)
    • 사실 iBeacon에서 가장 중요한 데이터는 UUID, Major, Minor 값입니다. 이 값을 추출한 뒤, 서버에 보내서 내가 어느 위치에 있는지, 이 비컨이 어떤 역할을 하는지, 그래서 사용자에게 어떤 정보를 보여주는 것이 좋은지 판단하게 됩니다. iBeacon 관련 공식 문서를 보면 주변에서 스캔한 UUID, Major, Minor 로 사용자의 위치를 판단하는 예시들을 언급하는데, iBeacon 장치가 위치기반 서비스의 일부임을 표시하기 위해  미리 정해둔 UUID 를 사용합니다. UUID는 꼭 표준에 지정된 값을 쓸 필요는 없으며, 서비스 개발자가 임의로 지정해서 사용해도 됩니다.
  • Major (2 byte), Minor (2 byte)
    • UUID 와 함께 사용자의 위치(Major, Minor = 지역, 세부 장소)를 판별하는데 주로 사용됩니다. 하지만 사용방법이 고정된 것은 아니며, 개발자가 이 값들을 임의의 목적으로 사용할 수도 있습니다. 예를 들어, 온도와 습도 데이터를 Major, Minor 데이터로 보낼 수도 있습니다. 물론 이 경우 비컨을 스캔하는 장치도 해당 데이터를 온습도로 인식하도록 만들어야겠죠.
  • TX Power
    • 비컨 장치가 신호를 송출할 때의 power 레벨을 여기에 적어 보내줍니다. 비컨 신호를 수신할 때 신호 세기를 알 수 있기 때문에 TX power 보다 얼마나 감소했는지를 계산하고, 대강의 거리를 짐작할 수 있습니다. 하지만 이렇게 계산된 거리는 대략적인 추정치입니다. 비컨 신호는 주변 상황이나 움직임, 장애물에 의한 변동이 심할 수 있습니다.

.

비컨 장치 제작과 테스트

이제 ESP32 모듈을 이용해 iBeacon 장치를 만들어 보겠습니다.

ESP32 모듈을 하나 준비하고, PC에 설치된 아두이노 개발환경에 ESP32 개발용 플러그인이 깔려 있어야 합니다. 아직 이 작업이 완료되지 않았다면 아래 링크를 참고해서 준비하세요.

그리고 ESP32 모듈이 비컨으로 동작할 수 있도록 스케치를 올려야 합니다. 아래 링크에서 소스를 받으세요.

아두이노 개발환경 상단 메뉴에서 [툴(Tools) -> 보드 -> Adafruit ESP32 Feather] 순서대로 선택해서 보드를 설정해줍니다. 만약 이후 과정에서 문제가 발생한다면 보드를 ESP32 Dev Module 또는 다른 ESP32 보드로 선택하세요.

상단 메뉴에서 [툴(Tools) -> 포트] 를 선택하세요. 그리고 업로드 버튼을 누르면 됩니다.

소스를 업로드 해도 ESP32 모듈에는 아무런 가시적인 변화가 없습니다. 무선으로 비컨 신호를 송출할 뿐이니까요. 그래서 비컨 신호를 스캔할 수 있는 안드로이드 앱이 하나가 필요합니다. nRF Connect 앱이 BLE 테스트 목적으로 사용하기 좋은 전문적인 안드로이드 앱니다. 구글 앱 스토어에서 nRF Connect 앱을 다운로드 받아 실행해 보세요. (Nordic Semiconductor 제작)

앱에서 주변 BLE 장치들을 스캔 했을 때 아래 처럼 우리가 제작한 iBeacon 기기가 보여야 합니다. 우리가 제작한 기기는 다음과 같은 UUID, Major, Minor 값을 송출합니다.

  • UUID : 4D6FC88B-BE75-6698-DA48-6866A36EC78E
  • Major : 0 (=0x00)
  • Minor : 스캔 할 때 마다 값이 증가 (비컨 기기가 리셋한 횟수)

손쉽게 비컨 장치를 만들었습니다!! ESP32 모듈이 비컨 장치로 동작함을 확인했으니 우리가 올린 스케치 코드를 확인해 보겠습니다. 스케치의 setup() 함수와 loop() 함수는 비교적 간단합니다. 로그 출력 부분을 빼면 아래 코드들이 보입니다.

.

void setup() {
  ......
  initBle();
  
  startAdvertising();
  Serial.println("Advertizing started...");
  delay(5000);
  
  stopAdvertising();

  // Deep sleep
  Serial.printf("enter deep sleep\n");
  delay(100);
  esp_deep_sleep(1000000LL * GPIO_DEEP_SLEEP_DURATION);
}

void loop() {
}

.

initBle() 함수를 이용해 BLE 기기로 동작할 준비를 합니다. 그리고 startAdvertising() 함수로 비컨 동작을 시작합니다. 5초간 동작 후 stopAdvertising() 으로 비컨 동작을 멈춥니다. 이후에 deep sleep 모드로 일정 시간동안 유지합니다. Deep sleep 이 끝나면 리셋되면서 리셋 카운터가 올라갑니다. 리셋 카운터는 Minor 값으로 사용됩니다.

BLE 장치로 동작하기 위해서는 initBle() 가 가장 먼저 호출되어야 합니다. 코드를 살펴보면 다음과 같습니다.

.

void initBle() {
  // Create the BLE Device
  BLEDevice::init("MyBeacon");
  // Create the BLE Server
  pServer = BLEDevice::createServer();
}

.

init() 함수를 실행해서 BLE 동작 준비를 하고 createServer() 를 호출해서 BLEServer 인스턴스를 얻습니다. 이때 BLEServer 인스턴스는 GATT server, Peripheral 장치, Slave 장치 역학을 하기 위한 인스턴스라 보면 됩니다.

initBle() 이후 호출되는 startAdvertising() 함수는 비컨으로 동작하기 위한 코드들을 담고 있습니다.

.

void startAdvertising() {
  // Create advertising manager
  pAdvertising = pServer->getAdvertising();

  // Set beacon data
  setBeacon();
  
  // Start advertising
  pAdvertising->start();
}

void setBeacon() {
  BLEBeacon oBeacon = BLEBeacon();
  oBeacon.setProximityUUID(BLEUUID(BEACON_UUID));
  oBeacon.setMajor((bootcount & 0xFFFF0000) >> 16);
  oBeacon.setMinor(bootcount&0xFFFF);
  oBeacon.setManufacturerId(0x4C00); // fake Apple 0x004C LSB (ENDIAN_CHANGE_U16!)
  
  BLEAdvertisementData oAdvertisementData = BLEAdvertisementData();
  BLEAdvertisementData oScanResponseData = BLEAdvertisementData();
  
  oAdvertisementData.setFlags(0x04); // BR_EDR_NOT_SUPPORTED 0x04
  
  std::string strServiceData = "";
  strServiceData += (char)26;     // Len
  strServiceData += (char)0xFF;   // Type
  strServiceData += oBeacon.getData(); 
  oAdvertisementData.addData(strServiceData);
  
  pAdvertising->setAdvertisementData(oAdvertisementData);
  pAdvertising->setScanResponseData(oScanResponseData);
}

.

먼저 BLEAdvertising 인스턴스를 얻고, 이 인스턴스에 각종 iBeacon 의 주요 데이터를 넣어줍니다. 비컨 장치를 세심하게 컨트롤 해야하는 상황이 아니라면 UUID, Major, Minor 값을 넣어주는 부분만 유심히 보셔도 됩니다. 여기서는 BEACON_UUID 값으로 “8ec76ea3-6668-48da-9866-75be8bc86f4d” 를 사용함을 주의하세요. 우리가 Scan 했을 때 보여지는 데이터와는 byte 순서가 역전되어 있습니다.

pAdvertising->start() 를 호출하면 ESP32 모듈이 비컨 신호를 송출하기 시작합니다. pAdvertising->stop()을 호출하기 전까지요.

.

void stopAdvertising() {
  // Stop advertising
  pAdvertising->stop();
}

.

비컨 신호를 송출하는 시간과 Deep sleep 을 유지하는 시간을 조절하면, 비컨 장치가 소비하는 전력을 우리가 원하는 수준으로 맞출 수 있습니다.

.

비컨을 스캔하는 센서장치

비컨 관련된 강좌글을 올리면 가장 많이 받는 질문 중 하나가 “BLE 모듈을 이용해서 주변 비컨 장치를 스캔해서 인식할 수 있느냐?” 입니다. 기존에 HM-10 모듈을 이용해서 이 작업을 하려면 제약사항도 많고 구현도 까다로웠는데, ESP32 모듈을 이용하면 훨씬 쉽게 이 기능을 구현할 수 있습니다.

ESP32 모듈 하나는 비컨으로 동작중이니, ESP32 모듈을 하나 더 준비하세요. 그리고 아래에 있는 스케치를 받아 새로 준비한 ESP32 모듈에 업로드 하세요.

이 스케치에는 주변 비컨 장치를 스캔해서 출력하도록 하는 코드가 들어가 있습니다. 업로드가 끝나면 [시리얼 모니터]를 실행하고 미리 만들어 둔 ESP32 비컨 장치가 스캔되는지 확인하세요. 시리얼 모니터 실행하면 오른쪽 아래 BAUDRATE 값을 115200 으로 바꿔줘야 합니다!!

Manufacturer data 가 비컨이 송신한 데이터입니다. 이 데이터가 0x4c00 으로 시작하면 이 장치가 iBeacon 임을 알 수 있습니다. 그리고 이어지는 데이터들을 보면 UUID, Major, Minor, TX power 값이 연이어 나옴을 알 수 있습니다. 주의할 점은, 우리가 사용한 Scan 기능은 iBeacon 장치 뿐 아니라 일반 BLE 장치도 찾아냅니다. 따라서 Manufacturer data 를 보고 우리가 원하는 장치 종류인지 필터링을 해야합니다.

예제로 살펴봤듯이 센서장치에서도 어렵지 않게 주변 BLE 장치를 스캔할 수 있습니다. 그리고 마음만 먹으면 iBeacon 장치가 쏘는 UUID/Major/Minor 값도 추출할 수 있습니다. 즉, 센서장치가 다른 센서장치의 비컨 신호를 수신해서 다양한 작업을 할 수 있는겁니다!! (BLE 통신 연결도 가능합니다. 이건 다음 파트에서 다룰겁니다.)

그럼 우리가 업로드 한 비컨 스캔 코드를 확인해 보겠습니다.

.

void setup() {
  initBle();
  scan();
}

void loop() {
  // put your main code here, to run repeatedly:
  delay(2000);
}

.

initBle() 함수를 이용해서 BLE 기능 사용할 준비를 했습니다. 이후 scan() 함수 호출하고 끝입니다.

scan() 함수는 다음과 같은 작업을 합니다.

.

void scan() {
  pBLEScan = BLEDevice::getScan(); //create new scan
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setActiveScan(true);            //active scan uses more power, but get results faster
  BLEScanResults foundDevices = pBLEScan->start(scanTime);

  Serial.print("Devices found: ");
  Serial.println(foundDevices.getCount());
  Serial.println("Scan done!");
}

.

BLEScan 인스턴스가 scan 작업을 위한 인스턴스입니다. setAdvertisedDeviceCallbacks() 함수를 이용해 콜백 함수를 등록해 주는 부분이 중요합니다. 여기서 콜백 함수를 등록해두면 기기가 스캔 될 때마다 콜백 함수가 호출됩니다. 아래는 우리가 등록한 콜백함수입니다.

.

class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
    void onResult(BLEAdvertisedDevice advertisedDevice) {
      Serial.printf("Advertised Device: %s \n", advertisedDevice.toString().c_str());
    }
};

.

onResult() 콜백 함수가 호출될 때, 전달받은 비컨 데이터를 분석해서 원하는 작업을 해주면 됩니다.

.

비컨 신호를 스캔하는 홈서버

이번엔 비컨 신호를 라즈베리파이 홈 서버에서 스캔하도록 만들어 보겠습니다. 만약 센서장치가 자유롭게 이동하는 물체에 부착되어 있다면, 라즈베리파이는 비컨 신호를 보고 어떤 장치들이 주변에 있는지, 대략 어느 정도 거리에 있는지 파악할 수 있겠죠.

[3-3 서버-센서장치 Classic BT 통신] 파트에서 라즈베리파이에 Classic BT/BLE 사용을 위한 준비를 했습니다. 그때는 PyBlueZ 라이브러리를 이용했는데, BLE 코드 구현을 위해서는 BluePy 라이브러리를 사용해야 합니다. 라즈베리파이에서 BT/BLE 를 사용하려면 꽤나 까다로운 드라이버/라이브러리 설정을 맞춰줘야 합니다. 아직 동작이 불안정한 부분도 있구요.

라즈베리파이에 접속해서 BluePy 라이브러리 설치를 위해 아래 명령어를 실행하세요.

  • sudo apt-get install python-pip libglib2.0-dev
  • sudo pip install bluepy

설치가 끝나면 아래 파이썬 코드를 받아서 라즈베리파이의 적당한 곳에 올려두세요.

아래처럼 실행하면 됩니다. 물론 비컨으로 만든 ESP32 모듈이 동작하고 있어야 합니다.

  • sudo python3 Scanner.py

아래처럼 비컨 스캔 결과가 표시된다면 모든것이 정상으로 동작하는 것입니다!

Advertising packet 의 데이터 중 Manufacturer 로 표시된 부분이 비컨 데이터입니다. 여기에 iBeacon UUID, Major, Minor, TX power 값이 모두 들어 있습니다.

비컨 신호를 스캔하는 파이썬 코드는 무척 간단합니다.

.

from bluepy.btle import Scanner, DefaultDelegate

class ScanDelegate(DefaultDelegate):
    def __init__(self):
        DefaultDelegate.__init__(self)
        

    def handleDiscovery(self, dev, isNewDev, isNewData):
        if isNewDev:
            print("Discovered device %s" % dev.addr)
        elif isNewData:
            print("Received new data from %s", dev.addr)

scanner = Scanner().withDelegate(ScanDelegate())
devices = scanner.scan(10.0)

for dev in devices:
    print("Device %s (%s), RSSI=%d dB" % (dev.addr, dev.addrType, dev.rssi))

    for (adtype, desc, value) in dev.getScanData():
        print("  %s = %s" % (desc, value))

.

Scanner().withDelegate() 를 이용해서 콜백함수를 등록하면 BLE 장치가 스캔되었을 때 콜백함수를 호출해줍니다.

scanner.scan() 을 이용해서 지정된 시간만큼 스캔을 돌릴 수 있습니다. 기기가 발견될 때 마다 withDelegate() 로 등록한 콜백함수가 호출되고, 스캔이 끝났을 때 발견된 BLE 장치들 리스트와 상세 정보가 리턴됩니다. 센서장치에서 스캔했을 때와 마찬가지로 Manufacturer data 를 분석하면 iBeacon 의 정보들을 얻을 수 있습니다.

BluePy 가 제공하는 API 에 대한 상세내용은 아래 링크를 참고하세요.

.

활용

비록 비컨 신호가 담을 수 있는 데이터는 매우 소량이지만, 이 데이터를 잘만 사용하면 다양한 서비스를 기획할 수 있습니다. 실제 야구장의 좌석 안내 서비스, 미술관의 작품 해설 서비스, 쿠폰이나 매장 이벤트 알림 서비스, 결제 서비스, 미아 방지 서비스 등에 비콘이 사용되고 있습니다.

복잡한 연결 설정이나 코드 없이도 주변 비컨 장치들을 짧을 시간 안에 스캔하고 정보를 얻을 수 있는 비컨의 특징을 본인만의 서비스로 구현해 보시기 바랍니다.

.

참고

주의!!! [사물 인터넷 네트워크와 서비스 구축 강좌] 시리즈 관련 문서들은 무단으로 내용의 일부 또는 전체를 게시하여서는 안됩니다. 계속 내용이 업데이트 되는 문서이며, 문서에 인용된 자료의 경우 원작자의 라이센스 문제가 있을 수 있습니다.

.

강좌 전체보기

.