Cocos2d-xでiBeacon発信機作ってみた

iBeaconを使ってやることがあったので、Cocos2d-xを使ってiOS/Android向けのiBeacon発信機を作ってみました。

#環境
* Cocos2d-x 3.15.1
* XCode Version 9.0 beta 2
* Android Studio 2.3

#プロジェクト作成
cocosコマンドを使ってプロジェクトを生成します。
Cocos2d-xの環境構築は他所のサイトを参考にしてください。
Macのターミナルを起動してコマンドを入力します。

```
cocos new BeaconTest -p jp.co.ienter.BeaconTest -l cpp
```
生成された`BeaconTest.xcodeproj`を起動します。

![s100.png](https://qiita-image-store.s3.amazonaws.com/0/154355/72ae269a-7c00-720e-0df4-29dec4c51bd4.png)

#iOSライブラリ追加
iOSで必要なライブラリを追加します。

* CoreBluetooth.framework
* CoreLocation.framework

![s101.png](https://qiita-image-store.s3.amazonaws.com/0/154355/1da9b017-25f9-0d68-4290-9085fc392145.png)

#AltBeacon導入
Androidで必要なライブラリを追加します。

* AltBeacon

build.gradle（Module: BeaconTest）に以下を記述します。
`jcenter()`と`compile 'org.altbeacon:android-beacon-library:2+'`を記述します。

```
repositories {
    jcenter()
}
```

```
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile project(':libcocos2dx')
    compile 'org.altbeacon:android-beacon-library:2+'
}

```

![s102.png](https://qiita-image-store.s3.amazonaws.com/0/154355/69bb0608-12e4-811c-9746-470d4acbd46b.png)

#データ保持プログラムを実装する
UUIDを端末内に保存するためのプログラムを実装します。
下記ファイルを新規作成してください。

* GameDataDM.cpp
* GameDataDM.hpp
* GameSettings.cpp
* GameSettings.hpp

GameDataDMはplist形式で端末内にデータを保存します。

```GameDataDM.hpp
#ifndef GameDataDM_hpp
#define GameDataDM_hpp

#include "cocos2d.h"

USING_NS_CC;

class GameDataDM;

using GameDataDMCallback = std::function<void (const bool &)>;

class GameDataDM : public cocos2d::Node
{
public:
    GameDataDM();
    virtual ~GameDataDM();
    virtual bool init();
    CREATE_FUNC(GameDataDM);
    
    ValueMap LoadBundlePlist(const char* pListName);
    void writeSaveFile(ValueMap data, GameDataDMCallback callback);
    
    CC_SYNTHESIZE(ValueMap, _dicItems, DicItem);    
};
```

```GameDataDM.cpp
#include "GameDataDM.hpp"

#define FILE_NAME "GameData"
////////////////////////////////////////////////////////////////////////////////
GameDataDM::GameDataDM()
: _dicItems(NULL)
{
}

////////////////////////////////////////////////////////////////////////////////
GameDataDM::~GameDataDM()
{
}
////////////////////////////////////////////////////////////////////////////////
bool GameDataDM::init()
{
    std::string str = StringUtils::format("%s", FILE_NAME);
    _dicItems = this->LoadBundlePlist(str.c_str());
    return true;
}
////////////////////////////////////////////////////////////////////////////////
ValueMap GameDataDM::LoadBundlePlist(const char *pListName)
{
    std::string pWorkStr = StringUtils::format("%s.%s.svd", "jp.co.ienter.BeaconTest", pListName);
    std::string savepath = FileUtils::getInstance()->getWritablePath() + pWorkStr.c_str();
    ValueMap dic;
    dic.clear();
    if(FileUtils::getInstance()->isFileExist(savepath)) {
        dic = FileUtils::getInstance()->getValueMapFromFile(savepath.c_str());
    }
    return dic;
}
////////////////////////////////////////////////////////////////////////////////
void GameDataDM::writeSaveFile(ValueMap data, GameDataDMCallback callback)
{
    std::string pWorkStr = StringUtils::format("%s.%s.svd", "jp.co.ienter.BeaconTest", FILE_NAME);
    
    //保存箇所のフルパスを取得
    std::string savepath = FileUtils::getInstance()->getWritablePath() + pWorkStr.c_str();
    //保存
    if(!FileUtils::getInstance()->writeToFile(data, savepath.c_str())) {
        log("save Failed:%s" , savepath.c_str());
        callback(false);
    } else {
        callback(true);
    }
}
```

セーブデータを呼び出すためのプログラムを記述します。

```GameSettings.hpp
#include "cocos2d.h"
#include "GameDataDM.hpp"
USING_NS_CC;
#define GS GameSettings::sharedSettings()

class GameSettings;

using GameSettingsCallback = std::function<void (const bool &)>;

class GameSettings {
public:
    ~GameSettings();
    GameSettings();
    static GameSettings* sharedSettings(void);
    
    void writeLocalData(GameSettingsCallback callback);
private:
    CC_SYNTHESIZE(GameDataDM*, _gameDataDM, GameDataDM);
    CC_SYNTHESIZE(ValueMap, _localData, LocalData);
    
    // UUID
    CC_PROPERTY(std::string, _uuID, UUID);
};
```

```GameSettings.cpp
#include "GameSettings.hpp"

#define USER_DEFAULTS_KEY_UUID      "UUIDKey"

static GameSettings* g_gameSettings = NULL;

GameSettings* GameSettings::sharedSettings() {
    if (! g_gameSettings) {
        g_gameSettings = new GameSettings();
        
    }
    return g_gameSettings;
}

GameSettings::~GameSettings()
{
}

GameSettings::GameSettings()
{
    _gameDataDM = GameDataDM::create();
    _localData = _gameDataDM->getDicItem();
}

// 保存処理
void GameSettings::writeLocalData(GameSettingsCallback callback)
{
    _gameDataDM->writeSaveFile(_localData, [=](bool flg)
    {
        callback(flg);
    });
}

// 保存されているUUID取得
std::string GameSettings::getUUID() const
{
    std::string ret = "";
    
    if (_localData.count(USER_DEFAULTS_KEY_UUID)) {
        ret = _localData.at(USER_DEFAULTS_KEY_UUID).asString();
    }
    return ret;
}

// 取得したUUIDを保存する
void GameSettings::setUUID(std::string var)
{
    _localData[USER_DEFAULTS_KEY_UUID] = var.c_str();
    this->writeLocalData([=](bool flg){});
}
```

#縦画面にする
AppDelegate.cppの21行目〜24行目をこのように修正します。
640x1136のサイズに固定しています。

```AppDelegate.cpp
static cocos2d::Size designResolutionSize = cocos2d::Size(640, 1136);
static cocos2d::Size smallResolutionSize = cocos2d::Size(640, 1136);
static cocos2d::Size mediumResolutionSize = cocos2d::Size(640, 1136);
static cocos2d::Size largeResolutionSize = cocos2d::Size(640, 1136);
```

#画面作成
スタート/ストップボタンを画面内に作成しましょう。

```HelloWorldScene.h
#include "cocos2d.h"
USING_NS_CC;

class HelloWorld : public cocos2d::Scene
{
public:
    static cocos2d::Scene* createScene();

    virtual bool init();
        
    // implement the "static create()" method manually
    CREATE_FUNC(HelloWorld);
private:
    void setup();
    void startTapped(MenuItemSprite* sender);
    void stopTapped(MenuItemSprite* sender);
};
```

```HelloWorldScene.cpp
#include "HelloWorldScene.h"
#include "BeaconBridge.hpp"
#include "GameSettings.hpp"

#define WINSIZE Director::getInstance()->getWinSize()
#define WINCENTER Point(WINSIZE.width*0.5, WINSIZE.height*0.5)

Scene* HelloWorld::createScene()
{
    return HelloWorld::create();
}

// on "init" you need to initialize your instance
bool HelloWorld::init()
{
    //////////////////////////////
    // 1. super init first
    if ( !Scene::init() )
    {
        return false;
    }
    
    this->setup();
    
    return true;
}

void HelloWorld::setup()
{
    // スタートボタン
    Sprite* btnStart_ON = Sprite::create();
    btnStart_ON->setTextureRect(Rect(0, 0, 200, 100));
    btnStart_ON->setColor(Color3B::GRAY);
    
    Sprite* btnStart_OFF = Sprite::create();
    btnStart_OFF->setTextureRect(Rect(0, 0, 200, 100));
    btnStart_OFF->setColor(Color3B(200, 50, 50));
    
    MenuItemSprite* btnStart = MenuItemSprite::create(btnStart_OFF,
                                                         btnStart_ON,
                                                         [this](Ref* sender) {this->startTapped((MenuItemSprite*)sender);});
    Label* lblStart = Label::createWithSystemFont("START", "Arial", 25);
    lblStart->setAlignment(TextHAlignment::CENTER);
    lblStart->setPosition(Vec2(btnStart->getContentSize().width*0.5, btnStart->getContentSize().height*0.5));
    btnStart->addChild(lblStart);
    
    // ストップボタン
    Sprite* btnStop_ON = Sprite::create();
    btnStop_ON->setTextureRect(Rect(0, 0, 200, 100));
    btnStop_ON->setColor(Color3B::GRAY);
    
    Sprite* btnStop_OFF = Sprite::create();
    btnStop_OFF->setTextureRect(Rect(0, 0, 200, 100));
    btnStop_OFF->setColor(Color3B(200, 200, 50));
    
    MenuItemSprite* btnStop = MenuItemSprite::create(btnStop_OFF,
                                                      btnStop_ON,
                                                      [this](Ref* sender) {this->stopTapped((MenuItemSprite*)sender);});
    Label* lblStop = Label::createWithSystemFont("STOP", "Arial", 25);
    lblStop->setAlignment(TextHAlignment::CENTER);
    lblStop->setPosition(Vec2(btnStop->getContentSize().width*0.5, btnStop->getContentSize().height*0.5));
    btnStop->addChild(lblStop);
    
    Menu* menu = Menu::create(btnStart, btnStop, NULL);
    menu->alignItemsVerticallyWithPadding(50);
    menu->setPosition(Vec2(WINCENTER.x, WINCENTER.y));
    this->addChild(menu);
    
    Label* lblUUID = Label::createWithSystemFont("", "Arial", 20);
    lblUUID->setAlignment(TextHAlignment::CENTER);
    lblUUID->setPosition(Vec2(WINCENTER.x, WINSIZE.height-20));
    this->addChild(lblUUID);

    // UUIDチェック
    if (GS->getUUID().length()) {
        lblUUID->setString(GS->getUUID());
        
        // Beacon初期化
        BeaconBridge::initBeacon(GS->getUUID());
        
    } else {
        // UUID取得する
        std::string uuid = BeaconBridge::initBeaconUUID();
        if (uuid.length()) {
            GS->setUUID(uuid);
            lblUUID->setString(GS->getUUID());
            
            // Beacon初期化
            BeaconBridge::initBeacon(GS->getUUID());
            
        }
    }

}

// ビーコン発信
void HelloWorld::startTapped(cocos2d::MenuItemSprite *sender)
{
    BeaconBridge::beaconAction(true, [=](bool flg)
    {
        log("start done!");
        
    });
    
}

// ビーコン停止
void HelloWorld::stopTapped(cocos2d::MenuItemSprite *sender)
{
    BeaconBridge::beaconAction(false, [=](bool flg)
    {
        log("stop done!");
    });
    
}
```

画面はこんな感じになります。

<img width="375" alt="s103.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/154355/5be67f78-e3d7-42ca-0443-c40a05ed4332.png">

#ビーコン処理を記述する(Android用)
まずはiOSとAndroidに渡すためのブリッジ関数を記述しましょう

```BeaconBridge.hpp
#include "cocos2d.h"

USING_NS_CC;

class BeaconBridge;

using BeaconCallback = std::function<void (const bool &)>;

class BeaconBridge
{
public :
    static void beaconAction(bool flg, BeaconCallback callback);
    static std::string initBeaconUUID();
    static void initBeacon(const std::string& uuid);
};
```

これはAndroid用の記述なので、XCodeには追加しないようにします。
jniの記述があるので、不明な場合はjniで検索してください。

```BeaconBridge.cpp
#include "BeaconBridge.hpp"
#include "platform/android/jni/JniHelper.h"
#define  CLASS_NAME "org/cocos2dx/cpp/AppActivity"

static BeaconCallback _beaconCallback;

void BeaconBridge::beaconAction(bool flg, BeaconCallback callback)
{
    _beaconCallback = nullptr;
    _beaconCallback = callback;
    
    JniMethodInfo t;
    
    if (JniHelper::getStaticMethodInfo(t, CLASS_NAME, "beaconAction", "(Z)V")) {
        t.env->CallStaticVoidMethod(t.classID, t.methodID, flg);
        
        // 解放
        t.env->DeleteLocalRef(t.classID);
    }
}

void BeaconBridge::initBeacon(const std::string& uuid)
{
    JniMethodInfo t;
    
    if (JniHelper::getStaticMethodInfo(t, CLASS_NAME, "initBeacon", "(Ljava/lang/String;)V")) {
        jstring aUUID = t.env->NewStringUTF(uuid.c_str());
        t.env->CallStaticVoidMethod(t.classID, t.methodID, aUUID);
        
        // 解放
        t.env->DeleteLocalRef(aUUID);
        t.env->DeleteLocalRef(t.classID);
    }
}

std::string BeaconBridge::initBeaconUUID()
{
    std::string ret;
    JniMethodInfo t;
    
    if (JniHelper::getStaticMethodInfo(t, CLASS_NAME, "initBeaconUUID", "()Ljava/lang/String;")) {
        jstring jStr = (jstring)t.env->CallStaticObjectMethod(t.classID, t.methodID);
        const char* str = t.env->GetStringUTFChars(jStr, NULL);
        ret = str;
        
        t.env->ReleaseStringUTFChars(jStr,str);
        t.env->DeleteLocalRef(t.classID);
    }
    
    return ret;
    
}


extern "C"
{
    // Javaから呼び出される
    void Java_org_cocos2dx_cpp_AppActivity_beaconActionCallback(JNIEnv *env, jobject thiz, jboolean flg)
    {
        if (_beaconCallback) _beaconCallback(flg);
    }
}

```

Android Studioを起動してプログラムを追記します。

```AppActivity.java

package org.cocos2dx.cpp;

import android.annotation.TargetApi;
import android.bluetooth.le.AdvertiseCallback;
import android.bluetooth.le.AdvertiseSettings;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;

import org.altbeacon.beacon.Beacon;
import org.altbeacon.beacon.BeaconParser;
import org.altbeacon.beacon.BeaconTransmitter;
import org.cocos2dx.lib.Cocos2dxActivity;

import java.util.UUID;

import static android.os.Build.ID;

public class AppActivity extends Cocos2dxActivity {
    private static Beacon beacon;
    private static BeaconTransmitter beaconTransmitter;
    private static AppActivity myref;
    private String wkUUID;

    public static synchronized AppActivity getInstance() {
        return myref;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        myref = this;

    }

    // UUID取得
    public static String initBeaconUUID()
    {
        return UUID.randomUUID().toString();
    }

    // Beacon初期化
    public static void initBeacon(String uuid)
    {
        beacon = new Beacon.Builder()
                .setId1(uuid)
                .setId2("1")  /*好きなIDを指定してください*/
                .setId3("80") /*好きなIDを指定してください*/
                .setManufacturer(0x004C)
                .build();

        BeaconParser beaconParser = new BeaconParser()
                .setBeaconLayout("m:2-3=0215,i:4-19,i:20-21,i:22-23,p:24-24");
        beaconTransmitter = new BeaconTransmitter(AppActivity.getInstance(), beaconParser);

    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public static void beaconAction(boolean flg)
    {
        if (flg) {
            //発信開始
            AppActivity.getInstance().runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    beaconTransmitter.startAdvertising(beacon, new AdvertiseCallback() {
                        @Override
                        public void onStartSuccess(AdvertiseSettings settingsInEffect) {
                            super.onStartSuccess(settingsInEffect);
                            Log.d("BeaconTest", "Beacon Success!");
                            beaconActionCallback(true);
                        }

                        @Override
                        public void onStartFailure(int errorCode) {
                            super.onStartFailure(errorCode);
                            beaconActionCallback(false);
                            Log.d("BeaconTest", "Beacon Failure!");
                        }
                    });

                }
            });
        } else {
            // 発信停止
            if (beaconTransmitter.isStarted()) {
                beaconTransmitter.stopAdvertising();
            }

            beaconActionCallback(true);
        }

    }

    @Override
    protected void onStop() {
        super.onStop();

        if (beaconTransmitter.isStarted()) {
            beaconTransmitter.stopAdvertising();
        }
    }

    private static native void beaconActionCallback(boolean flg);
}

```

AndroidManifest.xmlに権限を追記します。

```AndroidManifest.xml
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<!--対応していない機種にインストールされないようにuser-futureを設定する-->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
<!--Android 6.0でBLEを使うのに位置情報のパーミッションが必要-->
<uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION"/>
```

#ビーコン処理を記述する(iOS用)
XCodeにファイルを新規追加します。
iOSフォルダに保存してください。
c++の記述をするので、ファイル名の拡張子は`.mm`を使用しています。

```BeaconBridge.mm
#import "BeaconBridge.hpp"
#import "BeaconController.h"

// ビーコン発信・停止
void BeaconBridge::beaconAction(bool flg, BeaconCallback callback)
{
    [[BeaconController getInstance] beaconAction:flg callback:^(bool flg) {
        callback(flg);
    }];
}

// UUID生成
std::string BeaconBridge::initBeaconUUID()
{
    NSString* userID = [[UIDevice currentDevice].identifierForVendor UUIDString];
    return [userID UTF8String];
}

// ビーコン初期化
void BeaconBridge::initBeacon(const std::string& uuid)
{
    NSString *aUUID = [NSString stringWithCString:uuid.c_str()
                                           encoding:[NSString defaultCStringEncoding]];
    [[BeaconController getInstance] initBeacon:aUUID];
    
}
```

ビーコンの実装を記述します。

```Objc:BeaconController.h
#import <CoreBluetooth/CoreBluetooth.h>
#import <CoreLocation/CoreLocation.h>

@class BeaconController;

typedef void (^BeaconHandler)(bool flg);

@interface BeaconController : NSObject<CBPeripheralManagerDelegate>
{
    
}

@property (nonatomic, readonly) CBPeripheralManager* peripheralManager;
@property (nonatomic, readonly) NSString *uuid;

+ (BeaconController*) getInstance;
- (void)initBeacon:(NSString*)uuid;
- (void)beaconAction:(bool)flg callback:(BeaconHandler)callback;

@end;
```

```Objc:BeaconController.m
#import "BeaconController.h"

static BeaconController *instanceOfDialogHelper;

@implementation BeaconController
+ (BeaconController*) getInstance
{
    @synchronized(self)
    {
        if (instanceOfDialogHelper == nil)
        {
            instanceOfDialogHelper = [[BeaconController alloc] init];
        }
        
        return instanceOfDialogHelper;
    }
    
    // to avoid compiler warning
    return nil;
}

- (void) initBeacon:(NSString *)uuid
{
    _peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self
                                                                     queue:dispatch_get_main_queue()];
    _uuid = [uuid copy];
}

- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral
{
    
}

- (void) beaconAction:(bool)flg callback:(BeaconHandler)callback
{
    if (flg) {
        // ビーコン発信
        NSUUID* wkUUID = [[NSUUID alloc] initWithUUIDString:_uuid];
        CLBeaconRegion* region = [[CLBeaconRegion alloc] initWithProximityUUID:wkUUID
                                                                         major:1
                                                                         minor:80
                                                                    identifier:[wkUUID UUIDString]];
        
        NSDictionary* peripheralData = [region peripheralDataWithMeasuredPower:nil];
        [self.peripheralManager startAdvertising:peripheralData];
        callback(true);
    } else {
        // ビーコン停止
        [self.peripheralManager stopAdvertising];
        callback(true);
    }
}
@end
```

#まとめ
とても短いコードでiBeaconの発信機を作成することが出来ました。
iOSの方が発信感度？が良かったです。

githubに今回のコードをアップしていますので、ご確認ください。


https://github.com/hirophilip/BeaconTest