레이블이 블루투스인 게시물을 표시합니다. 모든 게시물 표시
레이블이 블루투스인 게시물을 표시합니다. 모든 게시물 표시

2019년 7월 9일 화요일

ESP32에서 블루투스(SPP profile) 사용하기

ESP32를 사용하면 아두이노에서처럼 따로 HC-05/06 모듈을 붙일 필요 없이 Bluetooth Classic 뿐 아니라 Bluetooth Low Energy(BLE)를 바로 사용할 수 있다.

여기서는 먼저 Bluetooth Classic 프로파일중에 하나인 SPP(Serial Port Profile)을 사용해 보겠다.

아두이노 IDE를 사용하면 Arduino core for ESP32에 필요한 라이브러리가 이미 들어있어 매우 간단하게 사용할 수 있다.

먼저 헤더파일을 include 해 줘야 한다.

#include "BluetoothSerial.h"

그 다음 BluetoothSeria 클래스의l 오브젝트를 선언해준다.

BluetoothSerial bt;

BluetoothSerial 클래스 오브젝트가 선언되면 시리얼 오브젝트와 거의 동일하게 사용할 수 있다. 일단 setup()에서 오브젝트를 초기화 시켜준다.

void setup()
{
  ...
  bt.begin("ESP32_SPP");    
   ...
}

Serial.begin()에서는 파라미터로 통신속도를 넘겨주는데 여기서는 bluetooth device name을 넘겨준다. 다른 기기에서 블루투스 장치를 검색할 때 이 이름으로 보이게 된다.

초기화가 끝나면 Serial 오브젝트와 동일하게 사용해 주면 된다. 즉 bt.read(), bt.write(), bt.available(), bt.print() 등을 Serial과 동일하게 호출할 수 있다.

여기서는 예제로 13번 핀에 LED가 연결되어 있을 때 블루투스로 '+'를 보내면 LED가 켜지고, '-'를 보내면 LED가 꺼지는 코드를 만들어 보겠다.

#include "BluetoothSerial.h"

BluetoothSerial bt;

void setup() {
  Serial.begin(115200);
  bt.begin("ESP32_SPP");
  Serial.println("Waiting for pairing");

  pinMode(13, OUTPUT);
}

void loop() {
 
  if (bt.available()) {
    ch = bt.read();
    if ('+' == ch) {
      digitalWrite(13, HIGH);
      bt.println("LED(13) turned ON");
    } else if ('-' == ch) {
      digitalWrite(13, LOW);
      bt.println("LED(13) turned OFF");
    }
  }
  delay(50);
}


위의 스케치를 ESP32에 업로드하고 시리얼 포트를 열면 다음과 같은 출력이 나올 것이다.


맨 아랫줄에 Waiting for pairing 이라고 출력을 해 블루투스로 연결할 준비가 되었음을 알 수 있다.

이제 컴퓨터 또는 안드로이드폰에서 블루투스 장치를 검색해보면 ESP32_SPP 라는 이름의 장치가 보일 것이다.


ESP32_SPP 를 연결해 주면 된다.

그 다음 시리얼 터미널 프로그램으로 가서 해달 포트를 오픈한다. 여기서는 맥이어서 'ESP32_SPP-ESP32_SPP_SER이라는 이름의 시리얼 포트가 된다.


포트를 오픈한 다음 '+' 또는 '-'를 입력해주면 각각 LED(13) turned ON 또는 LED(13) turned OFF가 터미널에 출력되며 ESP32의 13번 핀에 연결되어 있는 LED가 켜지거나 꺼지게 된다.
* 위의 화면에서는 입력된 글자를 확인하기 위해 local echo를 켜 놓았다. 즉 +/-는 사용자가 터미널에서 입력한 글자이고 LED(13) turned ON/OFF는 ESP32가 출력한 글자이다.






2016년 8월 22일 월요일

라즈베리 파이 3 시리얼/블루투스 문제 (Raspberry Pi 3 UART/Bluetooth problem)

라즈베리 파이에는 디버그 콘솔용 또는 시리얼 통신을 위한 핀이 40핀 커넥터에 나와 있다.

위의 그림에서 처럼 8/10번 핀이 각각 TxD, RxD가 된다.


위의 사진과 같은 USB-to-Serial(TTL level) converter를 사용해 PC와 연결해서 시리얼 통신을 할 수 있다.


이렇게 연결해 주면 된다.

컴퓨터에서 시리얼 에뮬레이터를 실행하고 통신속도를 115200bps로 맞춰 준 다음 라즈베리 파이에 전원을 공급하면 시리얼 에뮬레이터 화면에 이런 내용들이 출력되게 된다.

그런데 라즈베리 파이 2까지는 아무 문제가 없었던 것이 라즈베리 파이 3를 동일하게 연결하면 위와 같은 내용이 나오지 않고 이상하게 깨진 글자가 나오게 된다.

저렇게 깨진 글자가 나오게 되는 이유는 통신 속도가 틀려서이다.

원인을 이해하기 위해 라즈베리 파이의 내부 구조를 살펴보겠다.

라즈베리 파이 1B/B+는 BCM2835, 2는 BCM2836 SoC를 사용하고 있는데 여기에는 각각 2개의 UART가 들어 있는데 UART0는 Full UART, UART1은 mini UART이다. 여기서 UART0는 /dev/ttyAMA0로 사용되고 UART1은 /dev/ttyS0로 사용된다.

위의 그림처럼 mini UART는 몇가지 제약이 있지만, 40핀 커넥터의 8/10번 핀은 full UART의 Tx/Rx로 연결되어 있으므로 아무 문제가 없다.

하지만 라즈베리 파이 3로 오면서 블루투스가 추가되는 바람에 문제가 복잡해졌다.

먼저 하드웨어 구조를 살펴 보자.


위의 사진이 라즈베리 파이 1/2의 구조이다. 시리얼 포트 핀이 UART0에 연결되어 있다.


라즈베리 파이 3로 오면서 무선랜/블루투스 기능 추가를 위해 Broadcom의 BCM43438 칩을 사용하고 있는데 무선랜 기능은 SDIO로 연결하지만 블루투스 기능은 UART0으로 연결을 하고 시리얼 포트 핀은 UART1에 연결되도록 구조가 변경되었다. 그러므로 그 동안 아무 문제 없이 시리얼 포트를 사용하던 프로그램이 라즈베리 파이 3로 오면서 블루투스를 사용하게 되면 제대로 동작을 할 수 없게 되어 버린 것이다.

또한 코드의 /dev/ttyAMA0를 /dev/ttyS0로 변경한다고 해도 문제가 남는다.

UART1의 통신 속도는 다음 공식에 의해 결정된다.


라즈베리 파이 3의 경우 부팅시의 코어 동작 주파수는 400MHz이고 디폴트 baud rate는 115200 bps이다. 즉 위의 공식은...


이 된다. 여기서 baudRateReg는 부팅시에 코어 동작 주파수에 의해 계산된다.

그런데 전력소비를 줄이기 위해 frequency scaling 기능을 사용하고 있기 때문에 시스템 로드가 작은 경우 코어 동작 주파수를 250MHz로 줄일 수 있다. baudRateReg 값은 고정되어 있는데 시스템 클럭 주파수가 변동되기 때문에 결과적으로 baudrate도 따라서 바뀌게 되어 버린다.


즉 코어가 400MHz로 동작할 때는 115200bps로 동작하던 시리얼 포트가 코어 동작 속도가 250MHz가 되면 시리얼 포트 통신속도가 72004 bps로 바뀌어 버리기 때문에 위쪽의 그림처럼 깨진 글자가 나오게 되는 것이다.

그러므로 headless system (모니터 없이 사용하는 경우)에서 시리얼 디버그 콘솔을 사용하거나, UART를 사용하는 RPi shield(GPS/GSM, RS232 등등)를 사용하거나, 시리얼 포트로 다른 프로세서와 통신을 하는 경우 등에는 치명적인 문제가 될 수 있다.

이 문제의 해결책으로는 먼저 부팅시 코어 동작 속도를 고정해 버리는 것이다.

예전 펌웨어를 사용하는 경우 /etc/config.txt 파일에 "core_freq=250" 또는 "force_turbo=1"을 추가해 주는 것이다. 이렇게 하면 코어 동작 속도를 각각 최소 또는 최대(400MHz)로 고정시켜 버리게 된다. 최소로 고정시키면 RPi3의 성능에 영향을 미치게 되고, 최대로 고정시키면 방열대책을 신경 써 줘야 한다. 하지만 최대로 고정시켰다 하더라도 전압이 떨어지거나 코어 온도가 매우 높게 올라가면 250MHz로 쓰로틀링 될 수 있다는걸 주의해야 한다.

새 펌웨어의 경우에는 /boot/config.txt 파일에 "enable_uart=1"을 추가해주면 된다. 앞으로 모든 RPi distribution은 이 옵션을 다 지원하게 될 것이다. 이는 "core_freq=250"과 유사하지만 좀 더 명확한 의도를 표현한다. 이 옵션을 지원하는 distros인 경우 "enable_uart=1"이 활성화 되지 않으면 디버그 콘솔을 비활성화 시켜 놓는다. "enable_uart=1"에 대해 Raspberry Pi Foundation의 엔지니어 Phil Elwell이 상세히 설명 해 놓았다. https://github.com/raspberrypi/firmware/issues/553#issuecomment-199486644

두번째 방법으로는 디바이스 트리 오버레이로 40핀 헤더의 핀 매핑을 바꿔 Full UART를 사용하게 해 주는 것이다.

원래는 위의 사진에서 왼쪽같이 되어 있는데 오른쪽같이 되도록 변경하는 것이다. 이를 위해서는 '/boot/config.txt'에 'dtoverlay=pi3-disable-bt'를 추가해 주거나, '/boot/config.txt'에 'dtoverlay=pi3-miniuart-bt'를 추가해 주면 된다.

'dtoverlay=pi3-disable-bt'를 추가해주면 UART1을 사용하지 않는걸로 해서 블루투스를 비활성화 시킨다. 이 경우 라즈비안에서는 "sudo systemctl disable hciuart" 명령을 추가로 실행해 줘야 한다.

'dtoverlay=pi3-miniuart-bt'를 추가해 주는 경우는 블루투스가 mini-UART를 사용한다. 이를 위해 '/boot/config.txt'에 'core_freq=250' 또는 'force_turbo=1'이 들어가 있어야 한다. 또한 라즈비안의 경우 '/lib/systemd/system/hciuart.service'의 ttyAMA0를 ttyS0로 수정해 줘야 한다. 그리고 이 때는 블루투스의 성능에 제한이 생길 수 있기 때문에 저속 BLE만 실질적으로 사용하는데 문제가 없다.

위의 어떤 옵션을 사용하건 'enable_uart=0'을 넣어주지 않는 한 시리얼 콘솔은 활성화 되어 있다.

즉 위의 설명을 요약하자면 다음과 같다.

* 시리얼 콘솔과 일반적인 블루투스를 사용하고 싶은 경우 (/boot/config.txt 파일을 수정)
  - 코어 주파수를 고정
  - "core_freq=250" 또는 "force_turbo=1"을 추가 (old 펌웨어인 경우)
  - "enable_uart=1" (만일 동작 주파수를 고속으로 고정하고 싶으면 "force_turbo=1"을 추가)

* 고속 시리얼을 사용하고 싶은 경우
  - UART0와 UART1 핀을 교체
  - 블루투스를 사용할 수 없는 경우
    + "dtoverlay=pi3-disable-bt"
    + (라즈비안) "sudo systemctl disable hciuart"를 실행
  - 저속 블루투스 클래식 또는 BLE
    + "dtoverlay=pi3-miniuart-bt" 와 "core_freq=250" 또는 "force_turbo=1"중의 하나를 추가
    + (라즈비안) "/lib/systemd/system/hciuart.service" 파일의 "ttyAMA0"를 "ttyS0"로 수정
  - 디버그 콘솔을 비활성화 시키려면 "disable_uart=0" 추가
* Mini UART를 사용하려면 항상 코어 주파수를 고정할 필요가 있음


2016년 8월 11일 목요일

mbed BLE Button example

BLE_Button은 BLE service 템플릿이다. 단순한 boolean 값에 대한 read-only characteristic을 처리한다. 입력 소스는 보드에 있는 푸쉬버튼이다. 버튼이 눌리거나 놓을 때 characteristic의 값을 바꿔 준다.

이 코드는 다음의 기능을 다룬다.

* Advertising 및 connection 설정
* Input characteristic 만들기 : read-only, boolean with notification
* Service class 구축 및 BLE 스택에 추가하기
* 서비스와 characteristic에 UUID 할당
* Characteristic의 값이 바뀔 때 notification을 푸쉬하기

Running the application

Requirements

이 샘플 어플리케이션은 스마트폰의 어떤 BLE 스캐너에서도 볼 수 있다. 폰에 스캐너 앱이 없으면 다음의 앱을 설치하면 된다

* nRF Master Control Panel for Android
* LightBlue for iPhone

하드웨어 요구사항은 main readme 를 참고하면 된다.

* 주의: 두개 이상의 mbed board를 가지고 있다면 (nrf51dk 또는 mkit) BLE_LED 와 BLE_LEDBlinker를 동시에 실행시킬 수 있다. 더 상세한 내용은 BLE_LEDBlinker 데모를 참고하면 된다.

Build Instructions

온라인 mbed 컴파일러에서 이 예제를 빌드하고 싶으면, 먼저 오른쪽의 'Import' 버튼을 사용해 예제를 import 해야 한다.

그 다음 빌드한 코드를 실행하려는 플랫폼을 선택해 준다. 플랫폼은 예를 들어 NRF51-DK 같이 BLE를 지원하는 플랫폼이거나 또는 다음의 목록에 있는 것 중 하나여야 한다.

BLE를 지원하는 플랫폼 목록

또는 예를 들어 K64F나 NUCLEO_F401RE같은 보드에 X-NUCLEO-IDB04A1를 추가하고 그 하드웨어에 적합한 BLE 드라이버를 포함하는 지원 라이브러리를 설치해 줘야만 한다.

일단 플랫폼을 선택하고 나면 예제를 컴파일 한 후 바이너리 파일을 보드에 넣어주면 된다.


Checking for success

주의: 아래의 스크린 캡춰는 안드로이드에서 nRF Master Control Panel 버젼 4.0.5를 사용해 얻은 것이다. 버젼이 다르거나 아이폰을 사용하는 경우 버튼의 위치나 화면 레이아웃이 다를 수 있다.

* 어플리케이션을 빌드한 후 바이너리 파일을 보드에 설치
* 스마트폰에서 BLE 스캐너 앱을 실행

 * 스캔을 시작


* 디바이스를 찾는다. 'Button'이라는 이름을 가지고 있어야 한다.


 * 디바이스와 connection을 설정한다.


* 디바이스의 service와 characteristic을 검색한다. *Button service*는 UUID '0xA000'을 가지고 있고 이 서비스는 UUID가 '0xA001'인 *Button state characteristic* 를 포함하고 있다. 사용하는 스캐너에 따라 비표준 16-bit UUID는 128-bit UUID로 표시될수도 있다. 만일 이런 경우라면 다음 포맷이 사용된다.
'0000XXXX-0000-1000-8000-00805F9B34FB' 에서 'XXXX' 부분에 16-bit UUID 값이 들어간다.


* button state characteristic의 notification을 등록한다. 그러면 버튼의 상태가 바뀔 때 마다 버튼의 새 상태를 포함한 notification을 자동으로 받게 된다.


* 보드의 버튼 1을 누르면 버튼의 상태가 업데이트 되고 스캐너로 notification을 보낸다. 버튼 characteristic 값의 새 상태는 0x01 이어야 한다.


* 보드의 버튼 1을 누른걸 떼면 버튼의 상태가 업데이트 되고 스캐너로 notification을 보낸다. 버튼 characteristic 값의 새 상태는 0x00 이어야 한다.


예제의 전체 소스코드는 아래 링크에서 BLE_Button 디렉토리를 보면 된다.

https://github.com/ARMmbed/mbed-os-example-ble

핵심 코드는 아래의 두 파일(ButtonService.h, main.coo)이다.

이 예제를 템플릿으로서 약간만 수정하면 동시에 여러개의 버튼 입력 또는 아날로그 센서값을 쉽게 받아올 수 있을 것이다.

ButtonService.h


/* mbed Microcontroller Library
 * Copyright (c) 2006-2013 ARM Limited
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#ifndef __BLE_BUTTON_SERVICE_H__
#define __BLE_BUTTON_SERVICE_H__

class ButtonService {
public:
    const static uint16_t BUTTON_SERVICE_UUID              = 0xA000;
    const static uint16_t BUTTON_STATE_CHARACTERISTIC_UUID = 0xA001;

    ButtonService(BLE &_ble, bool buttonPressedInitial) :
        ble(_ble), buttonState(BUTTON_STATE_CHARACTERISTIC_UUID, &buttonPressedInitial, GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_NOTIFY)
    {
        GattCharacteristic *charTable[] = {&buttonState};
        GattService         buttonService(ButtonService::BUTTON_SERVICE_UUID, charTable, sizeof(charTable) / sizeof(GattCharacteristic *));
        ble.gattServer().addService(buttonService);
    }

    void updateButtonState(bool newState) {
        ble.gattServer().write(buttonState.getValueHandle(), (uint8_t *)&newState, sizeof(bool));
    }

private:
    BLE                              &ble;
    ReadOnlyGattCharacteristic<bool>  buttonState;
};

#endif /* #ifndef __BLE_BUTTON_SERVICE_H__ */

--------------------------------------------

main.cpp

/* mbed Microcontroller Library
 * Copyright (c) 2006-2013 ARM Limited
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
#include <mbed-events/events.h>

#include <mbed.h>
#include "ble/BLE.h"
#include "ble/Gap.h"
#include "ButtonService.h"

DigitalOut  led1(LED1, 1);
InterruptIn button(BLE_BUTTON_PIN_NAME);

static EventQueue eventQueue(
    /* event count */ 10 * /* event size */ 32
);

const static char     DEVICE_NAME[] = "Button";
static const uint16_t uuid16_list[] = {ButtonService::BUTTON_SERVICE_UUID};

ButtonService *buttonServicePtr;

void buttonPressedCallback(void)
{
    eventQueue.post(Callback<void(bool)>(buttonServicePtr, &ButtonService::updateButtonState), true);
}

void buttonReleasedCallback(void)
{
    eventQueue.post(Callback<void(bool)>(buttonServicePtr, &ButtonService::updateButtonState), false);
}

void disconnectionCallback(const Gap::DisconnectionCallbackParams_t *params)
{
    BLE::Instance().gap().startAdvertising(); // restart advertising
}

void blinkCallback(void)
{
    led1 = !led1; /* Do blinky on LED1 to indicate system aliveness. */
}

void onBleInitError(BLE &ble, ble_error_t error)
{
    /* Initialization error handling should go here */
}

void bleInitComplete(BLE::InitializationCompleteCallbackContext *params)
{
    BLE&        ble   = params->ble;
    ble_error_t error = params->error;

    if (error != BLE_ERROR_NONE) {
        /* In case of error, forward the error handling to onBleInitError */
        onBleInitError(ble, error);
        return;
    }

    /* Ensure that it is the default instance of BLE */
    if(ble.getInstanceID() != BLE::DEFAULT_INSTANCE) {
        return;
    }

    ble.gap().onDisconnection(disconnectionCallback);

    button.fall(buttonPressedCallback);
    button.rise(buttonReleasedCallback);

    /* Setup primary service. */
    buttonServicePtr = new ButtonService(ble, false /* initial value for button pressed */);

    /* setup advertising */
    ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::BREDR_NOT_SUPPORTED | GapAdvertisingData::LE_GENERAL_DISCOVERABLE);
    ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LIST_16BIT_SERVICE_IDS, (uint8_t *)uuid16_list, sizeof(uuid16_list));
    ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LOCAL_NAME, (uint8_t *)DEVICE_NAME, sizeof(DEVICE_NAME));
    ble.gap().setAdvertisingType(GapAdvertisingParams::ADV_CONNECTABLE_UNDIRECTED);
    ble.gap().setAdvertisingInterval(1000); /* 1000ms. */
    ble.gap().startAdvertising();
}

void scheduleBleEventsProcessing(BLE::OnEventsToProcessCallbackContext* context) {
    BLE &ble = BLE::Instance();
    eventQueue.post(Callback<void()>(&ble, &BLE::processEvents));
}

int main()
{
    eventQueue.post_every(500, blinkCallback);

    BLE &ble = BLE::Instance();
    ble.onEventsToProcess(scheduleBleEventsProcessing);
    ble.init(bleInitComplete);

    while (true) {
        eventQueue.dispatch();
    }

    return 0;
}


mbed BLE LED example

이 예제는 폰의 앱에서 BLE를 통해 LED의 on/off를 제어하기 위해 characteristic을 읽고 쓰는 코드이다.

이 코드는 다음의 기능을 다룬다.

* Advertising 및 connection 설정
* 서비스와 characteristic에 UUID 할당
* Input characteristic 만들기 : read/write, boolean. 이 characteristic으로 LED를 제어함
* Service class 구축 및 BLE 스택에 추가하기

Running the application

Requirements

이 샘플 어플리케이션은 스마트폰의 어떤 BLE 스캐너에서도 볼 수 있다. 폰에 스캐너 앱이 없으면 다음의 앱을 설치하면 된다

* nRF Master Control Panel for Android
* LightBlue for iPhone

하드웨어 요구사항은 main readme 를 참고하면 된다.

* 주의: 두개 이상의 mbed board를 가지고 있다면 (nrf51dk 또는 mkit) BLE_LED 와 BLE_LEDBlinker를 동시에 실행시킬 수 있다. 더 상세한 내용은 BLE_LEDBlinker 데모를 참고하면 된다.

Build Instructions


온라인 mbed 컴파일러에서 이 예제를 빌드하고 싶으면, 먼저 오른쪽의 'Import' 버튼을 사용해 예제를 import 해야 한다.

그 다음 빌드한 코드를 실행하려는 플랫폼을 선택해 준다. 플랫폼은 예를 들어 NRF51-DK 같이 BLE를 지원하는 플랫폼이거나 또는 다음의 목록에 있는 것 중 하나여야 한다.

BLE를 지원하는 플랫폼 목록

또는 예를 들어 K64F나 NUCLEO_F401RE같은 보드에 X-NUCLEO-IDB04A1를 추가하고 그 하드웨어에 적합한 BLE 드라이버를 포함하는 지원 라이브러리를 설치해 줘야만 한다.

일단 플랫폼을 선택하고 나면 예제를 컴파일 한 후 바이너리 파일을 보드에 넣어주면 된다.

Checking for success

주의: 아래의 스크린 캡춰는 안드로이드에서 nRF Master Control Panel 버젼 4.0.5를 사용해 얻은 것이다. 버젼이 다르거나 아이폰을 사용하는 경우 버튼의 위치나 화면 레이아웃이 다를 수 있다.

* 어플리케이션을 빌드한 후 바이너리 파일을 보드에 설치
* 스마트폰에서 BLE 스캐너 앱을 실행

* 스캔을 시작

* 디바이스를 찾는다. 'LED'라는 이름을 가지고 있어야 한다.

* 디바이스와 connection을 설정한다.

* 디바이스의 service와 characteristic을 검색한다. *LED service*는 UUID '0xA000'을 가지고 있고 이 서비스는 UUID가 '0xA001'인 *LED state characteristic* 를 포함하고 있다.

* *LED state* characteristic의 write 패널을 연다.

* 이 characteristic은 1 바이트 값을 허용한다.

* '0x01' : LED ON


* '0x00' : LED OFF

LED characteristic 값을 토글하면서 그 값에 따라 LED가 On/Off 되는걸 확인할 수 있다.

예제의 전체 소스코드는 아래 링크에서 BLE_LED 디렉토리를 보면 된다.

https://github.com/ARMmbed/mbed-os-example-ble

핵심 코드는 아래의 두 파일(LEDService.h, main.coo)이다.

이 예제를 템플릿으로 약간만 수정하면 동시에 여러개의 LED 또는 모터를 동시에 쉽게 제어할 수 있을 것이다.

LEDService.h


/* mbed Microcontroller Library
 * Copyright (c) 2006-2013 ARM Limited
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#ifndef __BLE_LED_SERVICE_H__
#define __BLE_LED_SERVICE_H__

class LEDService {
public:
    const static uint16_t LED_SERVICE_UUID              = 0xA000;
    const static uint16_t LED_STATE_CHARACTERISTIC_UUID = 0xA001;

    LEDService(BLEDevice &_ble, bool initialValueForLEDCharacteristic) :
        ble(_ble), ledState(LED_STATE_CHARACTERISTIC_UUID, &initialValueForLEDCharacteristic)
    {
        GattCharacteristic *charTable[] = {&ledState};
        GattService         ledService(LED_SERVICE_UUID, charTable, sizeof(charTable) / sizeof(GattCharacteristic *));
        ble.addService(ledService);
    }

    GattAttribute::Handle_t getValueHandle() const
    {
        return ledState.getValueHandle();
    }

private:
    BLEDevice                         &ble;
    ReadWriteGattCharacteristic<bool> ledState;
};

#endif /* #ifndef __BLE_LED_SERVICE_H__ */ 
 
------------------------------------------------------

main.cpp

 
/* mbed Microcontroller Library
 * Copyright (c) 2006-2013 ARM Limited
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <mbed-events/events.h>
#include <mbed.h>
#include "ble/BLE.h"
#include "LEDService.h"

DigitalOut alivenessLED(LED1, 0);
DigitalOut actuatedLED(LED2, 0);

const static char     DEVICE_NAME[] = "LED";
static const uint16_t uuid16_list[] = {LEDService::LED_SERVICE_UUID};

static EventQueue eventQueue(
    /* event count */ 10 * /* event size */ 32
);

LEDService *ledServicePtr;

void disconnectionCallback(const Gap::DisconnectionCallbackParams_t *params)
{
    (void) params;
    BLE::Instance().gap().startAdvertising();
}

void blinkCallback(void)
{
    alivenessLED = !alivenessLED; /* Do blinky on LED1 to indicate system aliveness. */
}

/**
 * This callback allows the LEDService to receive updates to the ledState Characteristic.
 *
 * @param[in] params
 *     Information about the characterisitc being updated.
 */
void onDataWrittenCallback(const GattWriteCallbackParams *params) {
    if ((params->handle == ledServicePtr->getValueHandle()) && (params->len == 1)) {
        actuatedLED = *(params->data);
    }
}

/**
 * This function is called when the ble initialization process has failled
 */
void onBleInitError(BLE &ble, ble_error_t error)
{
    /* Initialization error handling should go here */
}

/**
 * Callback triggered when the ble initialization process has finished
 */
void bleInitComplete(BLE::InitializationCompleteCallbackContext *params)
{
    BLE&        ble   = params->ble;
    ble_error_t error = params->error;

    if (error != BLE_ERROR_NONE) {
        /* In case of error, forward the error handling to onBleInitError */
        onBleInitError(ble, error);
        return;
    }

    /* Ensure that it is the default instance of BLE */
    if(ble.getInstanceID() != BLE::DEFAULT_INSTANCE) {
        return;
    }

    ble.gap().onDisconnection(disconnectionCallback);
    ble.gattServer().onDataWritten(onDataWrittenCallback);

    bool initialValueForLEDCharacteristic = false;
    ledServicePtr = new LEDService(ble, initialValueForLEDCharacteristic);

    /* setup advertising */
    ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::BREDR_NOT_SUPPORTED | GapAdvertisingData::LE_GENERAL_DISCOVERABLE);
    ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LIST_16BIT_SERVICE_IDS, (uint8_t *)uuid16_list, sizeof(uuid16_list));
    ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LOCAL_NAME, (uint8_t *)DEVICE_NAME, sizeof(DEVICE_NAME));
    ble.gap().setAdvertisingType(GapAdvertisingParams::ADV_CONNECTABLE_UNDIRECTED);
    ble.gap().setAdvertisingInterval(1000); /* 1000ms. */
    ble.gap().startAdvertising();
}

void scheduleBleEventsProcessing(BLE::OnEventsToProcessCallbackContext* context) {
    BLE &ble = BLE::Instance();
    eventQueue.post(Callback<void()>(&ble, &BLE::processEvents));
}

int main()
{
    eventQueue.post_every(500, blinkCallback);

    BLE &ble = BLE::Instance();
    ble.onEventsToProcess(scheduleBleEventsProcessing);
    ble.init(bleInitComplete);

    while (true) {
        eventQueue.dispatch();
    }

    return 0;
} 
 


2016년 5월 12일 목요일

라즈베리 파이를 비콘으로 사용하기 (Using Raspberry Pi as a Beacon)

독립적으로 비콘 모듈을 사용할 수도 있지만 이미 라즈베리 파이를 가지고 있다면 별도의 비콘 모듈 없이 라즈베리 파이를 비콘으로 설정해 사용할 수 있다.


라즈베리 파이 3의 경우 무선랜/블루투스가 기본 내장되어 있어 별도의 추가 하드웨어가 필요 없어 더 경제적이다. 라즈베리 파이 1/2의 경우는 USB타입의 블루투스 동글을 추가해 주면 된다.


 비콘은 특별한 것이 아니고 BLE(Bluetooth Low Energy)의 advertising packet 안에 옵션 필드인 Manufacturer Specific Data 에 특정 값을 집어넣어 주기적으로 advertising 하는 것이다. 그러므로 이 필드에 들어갈 값을 알아야 한다.

필드의 구조는 다음과 같다.

  • ID (uint8_t) : 이 값은 항상 0x02
  • Data Len (uint8_t) : payload 나머지 부분의 길이 → 0x15 (21 bytes)
  • 128-bit UUID (uint8_t[16]) : 128-bit 식별자
  • Major (uint16_t) : Major value
  • Minor (uint16_t) : Minor value
  • TX Power (uint8_t) : RSSI 기반으로 거리를 추정하기 위해 사용됨
 예를 들어 다음은 유효한 iBeacon payload의 값이다.


블루투스 표준에 의하면 Company Identifier가 Manufacturer Specific Data 필드 앞에 와야만 한다. 여기서 사용할 Apple의 company identifier는 0x004C 이다.

라즈베리 파이가 비콘 데이터를 보내게 하려면 몇가지 툴을 설치해 줘야만 한다. 여기서 핵심은 Bluez(리눅스 블루투스 스택)이고 부가적으로 libusb와 몇개의 helper library가 필요하다.

순서대로 필요한 툴들을 설치, bluez를 다운받아 컴파일 및 설치 해 주면 된다.

pi@raspberrypi ~ $ sudo apt-get install libusb-dev libdbus-1-dev libglib2.0-dev libudev-dev libical-dev libreadline-def

pi@raspberrypi ~ $ sudo mkdir bluez
pi@raspberrypi ~ $ cd bluez
pi@raspberrypi ~ $ sudo wget www.kernel.org/pub/linux/bluetooth/bluez-5.11.tar.xz
 
pi@raspberrypi ~ $ sudo unzx bluez-5.11.tar.xz 
pi@raspberrypi ~ $ sudo tar xvf bluez-5.11.tar
pi@raspberrypi ~ $ cd bluez-5.11
pi@raspberrypi ~ $ sudo ./configure --disable-systemd
pi@raspberrypi ~ $ sudo make
pi@raspberrypi ~ $ sudo make install



설치가 완료되었으면 블루투스 동글을 꼽고 재부팅 시켜주면 된다.

재부팅이 완료되면 먼저 시스템에 설치된 디바이스 목록을 확인한다.


pi@raspberrypi ~ $ hciconfig
hci0:  Type: BR/EDR    Bus: USB
       BD Address: 00:1A:7D:DA:71:12 ACL MTU: 310:10 SCO MTU: 64:8
       DOWN
       RX bytes: 495 acl:0 sco:0 events:21 errors: 0
       TX bytes: 89 acl:0 sco:0 commands: 21 errors: 0
pi@raspberrypi ~ $

만일 모든것이 정상적으로 설정되어 있으면 위와 같이 hci 디바이스가 보이게 된다. 이제 다음 명령으로 디바이스를 활성화 시켜 준다. 또한 advertising을 할 때 문제를 일으킬 수도 있으므로 device scanning은 꺼 준다.

pi@raspberrypi ~ $ sudo hciconfig hci0 up 
pi@raspberrypi ~ $ sudo hciconfig hci0 leadv3
pi@raspberrypi ~ $ sudo hciconfig hci0 noscan

이제 다시 hciconfig 명령을 수행해 보면 'DOWN' 대신 'UP  RUNNING'이 보여야 한다.

pi@raspberrypi ~ $ hciconfig
hci0:  Type: BR/EDR    Bus: USB
       BD Address: 00:1A:7D:DA:71:12 ACL MTU: 310:10 SCO MTU: 64:8
       UP RUNNING
       RX bytes: 1008 acl:0 sco:0 events:45 errors: 0
       TX bytes: 236 acl:0 sco:0 commands: 45 errors: 0
pi@raspberrypi ~ $

이제 마지막으로 해 줘야 할 일은 advertising data에 beacon 값을 넣어주는 것이다. 다음과 같이 명령을 입력해 준다. (좀 길지만 한 줄에 다 입력해 준다.)


pi@raspberrypi ~ $ sudo tools/hcitool -i hci0 cmd 0x08 0x0008 1E 02 01 1A 1A 
FF 4C 00 02 15 E2 0A 39 F4 73 F5 4B C4 A1 2F 17 D1 AD 07 A9 61 00 00 00 00 C8 00

위의 값들 중에서 FF는 Manufacturer Specific Data의 시작을 알려 주고, 4C 00은 Apple의 Company ID(0x004C), 그리고 나머지 부분부터 C8까지가 beacon의 payload이다.

이제 라즈베리 파이는 비콘으로 동작하게 된다. 확인해 보려면 iOS7 이후 버젼을 OS를 사용하는 아이폰/아이패드/아이팟 터치 또는 안드로이드 폰에 beacon toolkit 또는 유사한 앱을 설치해 실행하면 된다.

앱을 시작해 listen 모드로 들어가면 아래와 유사한 화면을 볼 수 있을 것이다.



이제 동작하는건 확인했지만 라즈베리 파이를 부팅할 때 마다 매번 위의 명령을 입력하기 힘드니 스크립트를 사용하는것이 편하다.

pi@raspberrypi ~ $ cat /home/pi/beacon.sh

#!/bin/bash
# ref: http://www.theregister.co.uk/Print/2013/11/29/feature_diy_apple_ibeacons/
set -x
# inquiry local bluetooth device
#hcitool dev
export BLUETOOTH_DEVICE=hci0
#sudo hcitool -i hcix cmd <OGF> <OCF> <No. Significant Data Octets> <iBeacon Prefix> <UUID> <Major> <Minor> <Tx Power> <Placeholder Octets>

#OGF = Operation Group Field = Bluetooth Command Group = 0x08
#OCF = Operation Command Field = HCI_LE_Set_Advertising_Data = 0x0008
#No. Significant Data Octets (Max of 31) = 1E (Decimal 30)
#iBeacon Prefix (Always Fixed) = 02 01 1A 1A FF 4C 00 02 15

export OGF="0x08"
export OCF="0x0008"
export IBEACONPROFIX="1E 02 01 1A 1A FF 4C 00 02 15"

#uuidgen  could gerenate uuid
export UUID="4a 4e ce 60 7e b0 11 e4 b4 a9 08 00 20 0c 9a 66"

export MAJOR="00 02"
export MINOR="00 01"
export POWER="C5 00"


# initialize device
sudo hciconfig $BLUETOOTH_DEVICE up
# disable advertising
sudo hciconfig $BLUETOOTH_DEVICE noleadv
# stop the dongle looking for other Bluetooth devices
sudo hciconfig $BLUETOOTH_DEVICE noscan

sudo hciconfig $BLUETOOTH_DEVICE pscan


sudo hciconfig $BLUETOOTH_DEVICE leadv

# advertise
sudo hcitool -i $BLUETOOTH_DEVICE cmd 0x08 0x0008 $IBEACONPROFIX $UUID $MAJOR $MINOR $POWER
sudo hcitool -i $BLUETOOTH_DEVICE cmd 0x08 0x0006 A0 00 A0 00 00 00 00 00 00 00 00 00 00 07 00
sudo hcitool -i $BLUETOOTH_DEVICE cmd 0x08 0x000a 01

echo "complete"


pi@raspberrypi ~ $

/home/pi/beacon.sh 라는 파일을 만들어 위의 파란색 부분의 내용을 넣어주면 된다. 위의 스크립트 중에 hcitool cmd 0x08 0x0006 a0 00 a0 00 .. 에서 첫번째/두번째 바이트 (0xa0 0x00)은 minimum interval, 세번째/네번째 바이트(0xa0 0x00)은 maximum interval이다. 0xa0 0x00은 0x00a0 (big endian)이 되므로 10진수로는 160이 된다. BLE에서 시간 granularity는 0.625ms이므로 최소/최대 인터벌은 모두 160 * 0.625ms = 100ms가 된다. 즉 매 0.1초마다 한번씩 비콘 신호를 advertising하게 된다.

파일을 만들었으면 chmod 명령으로 실행 퍼미션을 추가해 준다.

pi@raspberrypi ~ $ chmod +x beacon.sh

이제부터는 ./beacon.sh 만 입력해 주면 라즈베리 파이가 비콘으로 동작한다.

pi@raspberrypi ~ $ ./beacon.sh
+ export BLUETOOTH_DEVICE=hci0
+ BLUETOOTH_DEVICE=hci0
+ export OGF=0x08
+ OGF=0x08
+ export OCF=0x0008
+ OCF=0x0008
+ export 'IBEACONPROFIX=1E 02 01 1A 1A FF 4C 00 02 15'
+ IBEACONPROFIX='1E 02 01 1A 1A FF 4C 00 02 15'
+ export 'UUID=4a 4e ce 60 7e b0 11 e4 b4 a9 08 00 20 0c 9a 66'
+ UUID='4a 4e ce 60 7e b0 11 e4 b4 a9 08 00 20 0c 9a 66'
+ export 'MAJOR=00 01'
+ MAJOR='00 01'
+ export 'MINOR=00 00'
+ MINOR='00 00'
+ export 'POWER=C5 00'
+ POWER='C5 00'
+ sudo hciconfig hci0 up
+ sudo hciconfig hci0 noleadv
+ sudo hciconfig hci0 noscan
+ sudo hciconfig hci0 pscan
+ sudo hciconfig hci0 leadv
+ sudo hcitool -i hci0 cmd 0x08 0x0008 1E 02 01 1A 1A FF 4C 00 02 15 4a 4e ce 60 7e b0 11 e4 b4 a9 08 00 20 0c 9a 66 00 01 00 00 C5 00
< HCI Command: ogf 0x08, ocf 0x0008, plen 32
  1E 02 01 1A 1A FF 4C 00 02 15 4A 4E CE 60 7E B0 11 E4 B4 A9
  08 00 20 0C 9A 66 00 01 00 00 C5 00
> HCI Event: 0x0e plen 4
  01 08 20 00
+ sudo hcitool -i hci0 cmd 0x08 0x0006 A0 00 A0 00 00 00 00 00 00 00 00 00 00 07 00
< HCI Command: ogf 0x08, ocf 0x0006, plen 15
  A0 00 A0 00 00 00 00 00 00 00 00 00 00 07 00
> HCI Event: 0x0e plen 4
  01 06 20 0C
+ sudo hcitool -i hci0 cmd 0x08 0x000a 01
< HCI Command: ogf 0x08, ocf 0x000a, plen 1
  01
> HCI Event: 0x0e plen 4
  01 0A 20 0C
+ echo complete
complete 

pi@raspberrypi ~ $

이제 라즈베리 파이를 비콘으로 사용하려면 부팅될 때 마다 자동으로 위의 스크립트가 실행되도록 설정해 주면 된다.

라즈베리 파이에서 특정 프로그램이 부팅될 때 실행되게 하는 방법은 몇가지가 있지만 /etc/rc.local을 수정하는 방법 또는 cron을 사용하는 방법은 문제가 있다. 라즈베리 파이에서 블루투스가 활성화 된 이후에 위의 스크립트가 실행되어야만 하는데 두가지 방법은 스크립트가 먼저 실행되어 버리기 때문에 비콘으로 동작하지 못한다.

그러므로 systemd를 사용해 준다. 먼저 /lib/systemd/system 디렉토리에 두개의 파일을 만들어 준다. 각각 파일의 내용은 아래와 같다.

pi@raspberrypi ~ $ cat /lib/systemd/system/beacon.service
[Unit]
Description=Beacon service
After=bluetooth.target

[Service]
Type=forking
ExecStart=/home/pi/beacon.sh
ExecReload=/home/pi/beacon.sh

[Install]
WantedBy=bluetooth.target


pi@raspberrypi ~ $ cat /lib/systemd/system/beacon.target
#  This file is part of systemd.
#
#  systemd is free software; you can redistribute it and/or modify it
#  under the terms of the GNU Lesser General Public License as published by
#  the Free Software Foundation; either version 2.1 of the License, or
#  (at your option) any later version.

[Unit]
Description=Beacon
Documentation=man:systemd.special(7)
StopWhenUnneeded=yes


pi@raspberrypi ~ $ sudo systemctl daemon-reload
pi@raspberrypi ~ $ sudo systemctl enable beacon.service
Synchronizing state for beacon.service with sysvinit using update-rc.d...
Executing /usr/sbin/update-rc.d beacon defaults
Executing /usr/sbin/update-rc.d beacon enable


pi@raspberrypi ~ $

이제 시스템을 리부팅 하면 항상 자동으로 비콘이 시작된다.





아이폰에서 Beecon 앱으로 확인해 본 결과이다. 스크립트에서 설정한 UUID(4a4e......9a66) , Major(2), Minor(1) 를 확인할 수 있다.






2015년 1월 30일 금요일

HM-10 iBeacon 제작

이전 포스트(HM-10 Bluetooth 모듈을 iBeacon으로 사용하기)에서 소개한대로 CR2032 배터리를 사용해 소형 iBeacon 모듈을 제작해 보았다.



만능기판 위에 HM-10 모듈을 약간 두께가 있는 양면테입으로 붙여주고 뒷면에 배터리 홀더를 붙여 주었다. 위쪽 사진 우측 상단의 커넥터는 모듈에 명령을 주기 위해 USB-to-Serial 컨버터를 연결하기 위한 것이고 설정이 끝나면 케이블은 분리하고 모듈 자체적으로 동작하게 된다.


제작한 iBeacon의 크기 비교이다. 위의 사진처럼 500원짜리 동전보다 조금 큰 정도의 크기이다. 이렇게 만든 경우 배터리 한개로 대략 55일 정도 동작하게 된다.

커넥터를 연결하고 컴퓨터에서 시리얼 터미널 에뮬레이터를 동작시켜 명령을 내려준다. (통신 파라미터는 9600-N81)


이전 포스트에서 이야기 했던것처럼 명령어 입력이 끝난걸 엔터키로 확인하는게 아니고 타이밍을 가지고 하기 때문에 위와 같이 줄바꿈이 되지 않고 명령어, 응답이 계속 붙어 보이게 된다.

AT+VERR?HMSoft V533AT+RENEWOK+RENEWAT+RESETOK+RESETATOKAT+MARJ0x1234OK+Set:0x1234AT+MINO0xFA01OK+Set:0xFA01AT+ADVI5OK+Set:5AT+NAMEARSVIATOROK+Set:ARSVIATORAT+ADTY3OK+Set:3AT+IBEA1OK+Set:1AT+DELO2OK+DELO2AT+PWRM0OK+Set:0AT+RESETOK+RESET

쉽게 볼 수 있게 색깔로 구분해 놓은 것이다. 빨간색 볼드는 입력한 명령어, 파란색 이태릭체는 모듈의 응답이다. 

명령 입력이 끝났으면 이제 케이블을 분리해도 된다. 

실제로 정상 동작하는지 확인해 보겠다.



 아이폰에서 Locate 앱을 실행시켜 본 결과이다. 디폴트 UUID는 변경하지 않았으므로 Apple AirLocate로 보인다. 모듈에 설정해 준 대로의 Major 0x1234 (십진수로 4660), Minor 0xFA01 (십진수로 64001)를 확인할 수 있다.


Apple AirLocate.... 부분을 클릭하면 상세정보를 볼 수 있다. 


2015년 1월 28일 수요일

HM-10 Bluetooth 모듈을 iBeacon으로 사용하기

Aliexpress등에서 매우 저렴하게 구입할 수 있는 Bluetooth 4.0 Low Energy (BLE) 모듈인 HM-10을 iBeacon으로 사용할 수 있다. 물론 HM-10은 데이터 통신 또는 단독 센서, 디바이스 제어등의 용도로 사용할수도 있지만 여기서는 그것들에 대한 설명은 생략한다. 

HM-10은 TI의 CC2540 또는 CC2541 BLE SoC 기반의 Bluetooth 4.0 모듈이다. 개당 약$6~7 정도의 가격이다. 펌웨어 버젼은 2014년 9월 기준으로 펌웨어 버젼 531이 나와있다.



모듈은 위의 사진에서 볼 수 있는것처럼 매우 작다. 접촉패드 간격은 1.5mm이라 일반 브레드보드나 만능기판등에서 바로 사용하기는 힘들다. 전원에 연결하거나, 모듈을 iBeacon으로 설정하기 위해 PC에 연결된 USB-to-Serial 케이블에 납땜을 할 때 매우 주의해야만 한다. HM-10은 2.0~3.7V에서 동작하고 I/O핀은 3.3V tolerant이다.

Hardware Connections


모듈에 전원을 공급하기 위해 두가닥의 선이 필요하고, USB-to-Serial adapter를 통해 모듈을 설정하기 위해 임시로 3개의 선이 필요하다. 



위의 사진은 모듈이 3개의 NiMH 충전 배터리가 들어있는 배터리 팩(1.2V * 3 = 3.6V) 과 USB-to-Serial adapter에 연결되어 있는걸 보여준다. Arduino를 사용하고 있다면 아마도 이런 류의 adapter를 이미 가지고 있을것이고, 그렇지 않다면 약 $15정도로 구입할 수 있을 것이다. 만일 구입해야 한다면 3.3V/5V 점퍼나 선택 스위치가 있는 것으로 구입하길 권한다. 



HM-10은 평균적으로 약 0.18mA의 전류를 사용한다. 사진의 배터리팩은 약 1800mAh의 용량을 가지고 있으므로 약 10000시간(즉 1년 이상) 지속될 수 있다. 만일 매우 작은 iBeacon을 만든다면 240mAh 용량의 CR2032 코인셀을 사용할 수도 있고, 이 경우 약 55일정도 사용할 수 있다. 



위의 다이어그램은 케이브블을 어떻게 연결해야 하는지 보여준다. 배터리는 모듈의 12(+), 13(-)번 핀에 연결되어 있다. RS232 adapter의 RxD는 1번핀에, TxD는 2번핀에 연결한다. Adapter의 그라운드 핀은 모듈의 14번 핀에 연결한다. 

Using a Terminal Emulator to talk to the HM-10


하드웨어가 준비되었으면 HM-10이 동작하는지 확인할 수 있다. 배터리와 serial adapter가 HM-10에 연결되었으면 adapter의 USB케이블을 PC에 연결한다. 처음으로 adapter를 사용하는 것이면 adapter의 디바이스 드라이버를 설치하도록 요구받을 수도 있다. 대부분의 경우 OS가 자동으로 드라이버를 찾아준다. 

HM-10은 AT 명령셋을 사용하는데 매우 특이한 타이밍을 요구한다. 명령은 CR이나 LF로 끝나는것이 아니고, 명령이 완전히 다 입력된 다음의 매우 짧은 딜레이에 의존한다. 그러므로 arduino 소프트웨어의 시리얼 모니터를 이용하기를 권장한다. 이 글을 읽는 대다수는 arduino IDE가 이미 설치되어 있을것이라고 생각한다.



Tools 메뉴에서 Serial Port를 선택하고 거기에서 자신의 serial adapter에 해당하는 COM 포트를 선택한다. 대부분의 경우 가장 아래쪽의 COM포트이다. 위의 스크린샷에서는 COM3이다. 

그 다음에 Tools 메뉴에서 Serial Monitor를 실행한다. 아래 메뉴에서 “9600 Baud”와 “No line ending”을 선택한다. 이제 HM-10과 이야기 할 수 있다. “AT” 뒤에 엔터키를 타이핑 해 본다. HM-10이 “OK”라고 응답해야 한다. HM-10은 출력하는 내용 뒤에 LF로 끝나지 않기 때문에 모든 응답 메시지는 뒤에 계속 추가된다.



다음으로 HM-10의 펌웨어 버젼을 확인한다. “AT+VERR?”를 입력한다. 최근에 HM-10을 구입했으면 버젼은 526 또는 그 이후 버젼일것이다. 그렇다면 아래의 Firmware Update 부분은 건너 뛰고 iBeacon Configuration을 가도 된다.

Firmware update

펌웨어 업데이트가 필요하면, 먼저 JNHuaMao 사이트에서 적합한 버젼을 다운로드 받아야 한다. 자신이 가지고 있는 HM-10 모듈이 어떤 칩(2540인지 2541인지)을 사용하는가를 확인한다.  초기모듈은 CC2540을 사용했고 신형 모듈은 CC2541을 사용한다. (최근에 구입했다면 대부분은 CC2541을 사용했을 것이다.)


위의 링크에서 펌웨어를 다운받으면 된다. 다운받았으면 압축을 해제해준다. 최소 3개의 파일이 있을 것이다. readme.txt 파일에는 업데이트 하는 설명이 들어있다. .bin으로 끝나는 파일이 업데이트 할 실제 펌웨어이다. 그리고 .exe로 끝나는 실행파일은 펌웨어를 업데이트 해 주는 프로그램이다. 

1. Arduino 시리얼 모니터에서 ‘AT+SBLUP’를 입력해 준다. 그러면 ‘OK+SBLUP’라고 응답할 것이다. 이제 HM-10은 펌웨어 업데이트를 할 준비가 된 것이다.
2. 시리얼 모니터를 종료한다.
3. 펌웨어 업데이트 프로그램을 실행한다. 
4. COM 포트에 HM-10이 연결되어 있는 시리얼 포트 번호를 설정한다.
5. ‘…’ 버튼을 클릭해 다운로드 한 .bin 파일을 선택한다.
6. 마지막으로 ‘Load Image’ 버튼을 클릭한다.
7. 펌웨어 업데이트는 몇분이 걸린다. 업데이트 하는 도중에는 중단하면 안된다. 
8. Verification이 끝나면 ‘Download completed successfully’라는 메시지가 나올 것이다.



이제 업데이트가 끝났으니 펌웨어 업데이트 프로그램을 종료하면 된다.

iBeacon Configuration


HM-10을 iBeacon으로 설정하려면 몇가지 명령을 입력해 줘야 한다. Arduino Serial Monitor 프로그램을 사용해 명령을 입력해 준다. 아래 리스트에서 볼드로 되어 있는 부분이 실제 입력해줘야 하는 명령이고 그 뒷부분은 각 명령이 어떤 동작을 하는지에 대한 설명으로 입력하면 안된다. 각 명령은 “OK …”으로 ack 되어야 한다. 

1.  AT+RENEW       Factory default로 복구
2.  AT+RESET       Reboot
3.  AT             OK를 기다림
4.  AT+MARJ0X1234  Major no.를 0x1234로 설정
5.  AT+MINO0XFA01  Minor no.를 0xfa01로 설정
6.  AT+ADVI5       Advertising interval을 5(546.25 ms)로 설정
7.  AT+NAMEDOPEY   HM-10 모듈 이름을 DOPEY로 설정
8.  AT+ADTY3       non-connectable로 설정 (전원 절약)
9.  AT+IBEA1       iBeacon 모드 활성화
10. AT+DELO2       iBeacon broadcast-only로 설정 (전원 절약)
11. AT+PWRM0       Auto-sleep 활성화. 8에서 0.18mA로 전력소비를 줄여줌
12. AT+RESET       Reboot

위의 명령들을 다 입력했다면 HM-10은 아이폰이나 안드로이드에서 iBeacon으로 보여야 한다. 4~5단계에서 적절한 major와 minor no.를 선택할 수 있다. Major no.는 특정 범위(스토어 또는 빌딩)내에서 동일하고, minor no.가 iBeacon을 유일하게 식별한다. 위의 프로시져는 HM-10의 디폴트 UUID(표준 proximity UUID)는 변경하지 않는다. UUID를 변경하고 싶으면 AT+IBE0, AT+IBE1, AT+IBE2, AT+IBE3 명령을 사용할 수 있다. 16버이트 UUID는 4바이트 단위로 나뉘어 각각은 다른 명령으로 바꿀 수 있다. 아래 테이블은 UUID를 4개의 명령으로 각각 어느 부분을 변경할 수 있는지 보여준다. 



Removing the Programming Cable


HM-10이 정상적으로 iBeacon으로 동작하는게 확인되었으면 serial adapter에 납땜질 된 케이블을 제거하고, 전원 케이블만 연결해 놓으면 된다. 



배터리 팩과 모듈을 보호하기 위해 케이스에 집어넣어 준다.



Simple iBeacon Scan

아이폰이나 안드로이드 폰에서 iBeacon 스캔을 할 수 있는 앱을 실행시켜 본 결과이다. 위와 같이 정상 동작하고 있음을 확인할 수 있다. 단 iBeacon과의 거리는 RSSI값을 가지고 계산한 것이기 때문에 상대적인 거리이지 정확한 거리는 아님을 주의해야 한다. 

Using the HM-10 as an iBeacon Proximity Device


다음의 8개 UUID는 Apple AirLocate에 proximity device로 들어있다. AirLocate를 사용하는 iOS app과 호환성을 보장하기 위해 이 값을 사용한다.




Waking Up the HM-10


HM-10이 auto-sleep 모드로 설정되어 있으면 전원이 들어오거나 리부팅 될 때 빨리 sleep모드로 갈 수 있다. AT+PWRM0 명령을 보낸 뒤에 재시작하면 MH-10은 더이상 AT 명령에 응답하지 않는다. 디바이스를 깨우려면 80글자 이상의 랜덤 알파벳을 보내면 된다. 그러면 모듈이 깨어나 OK+WAKE라고 응답한다. 한동안 디바이스에 명령을 내려야 하면 잠들지 않게 하기 위해 먼저 AT+PWRM1 명령을 보내준다. 명령이 끝나면 전력소비를 줄이기 위해 AT+PWRM0을 보내면 된다. 


To be continued....   HM-10 iBeacon 제작