勉強と趣味メモブログ

雑多なブログ

iBeaconでの広告配信と来客情報表示システムを開発した話

まずはじめに

こんにちは。初記事でどう書こうかまだよくわかっていないaoking21です。

今回の記事では、新潟県IT&ITS推進協議会主催の「にいがた暮らしIoTアイデア展2017」に提出した 「広告配信の新しいカタチ〜ここくる〜」の制作過程を忘れないように書いておこうと思います。

まずシステムの担当部分についてお話しする前に、一緒に開発をしたメンバーについても触れたいと思います。
もともと私は大学で団体を結成し、2017年4月から当時3年の私と、1年生3名の計4名で活動していました。
今回の作品の制作期間は書類審査を通過した同年10月から作品提出期限の翌年2月まででしたが、1年生はC言語の学習を終えていなかったため、 結果的にはデザイン部分の担当をお願いしました。(Webページのレイアウトやカラー配置など)
結果として、私はデザイン以外の全てを担当しました。

話は変わって最終的なコンテスト結果ですが、今回のコンテストのテーマが「暮らしIoT ~日常生活において便利で役立つ暮らしの実現~」 となっていました。よくよく考えると今回考えた作品はこのテーマから少し外れていたのもあったかもしれませんが、 コンテストの結果としては優秀賞を取ることができませんでした。しかし、システム経験の表面的な部分に広い分野から関われたので 良い経験だったと思うことにしています。そう思ってもいます。
それでは実際に今回のシステムの説明に入りたいと思います。

そういえばこの記事の内容ですが、実際に試してどのような不都合が起きても読者様の自己責任でよろしくお願いします。

システム概要

概念図.png 全体の概念図です。ご覧の通り色々と大雑把な作りになっています。ちなみに後ほど触れる理由と同じように、サーバーにはRaspberry Pi 3 model Bを使用しました。

今回のシステムの概要ですが、タイトルの通り、iBeaconを用いて広告の配信をするものです。
さらに、配信側が操作するWebページでは広告を実際に受信したのべ人数を表示できるようになっています。
わかってる人ならRaspberry Piを使用しなければいけない理由がどこにもないことに気がつくと思います。
新潟県IT&ITS推進協議様より、Raspberry Piの購入費用をご負担いただけるとのことで iBeaconの発信端末をRaspberry Pi zero Wとしました。

利用の流れ

このシステムを利用する際の経営者側の大まかな流れですが、

  1. 経営者用Webページにログイン
  2. 設定中の広告情報と来客情報を取得してページを表示
  3. 配信する広告の登録

といった流れになっています。 次に広告を受信する来店者側ですが、

  1. 店舗に近づいたら通知を表示
  2. 受信可能エリア内でアプリを開いたら、店舗のiBeaconの情報を取得
  3. 取得した情報からPHPで作成したAPIに来客者の情報が入ったJSONをPOSTする
  4. APIから店舗の広告情報をJSONで受け取る
  5. JSONから店舗の広告情報を取得して表示する

以上のような流れになります。

目標

今回のシステムの目標を大雑把に並べると

  1. 広告をスマートフォンに送信できる
  2. 受信可能エリアに入ったら通知を出す
  3. 広告の更新が即座に反映される
  4. 広告受信者をカウントし、Webページでグラフに表示する

といったような目標を設定し、これらの機能を最低でも実装させてから完成とみなしました。

使用した言語とミドルウェア

今回のシステム開発に使用した言語とミドルウェアは以下のものです。

・Webサーバー

Apache

・データベースサーバー

MySQL

・バックエンド

PHP

・フロントエンド

HTML/CSS

・ネイティブアプリ

Swift3 (手持ちの端末がiOSだったので)

実際のコード

実際に書いたコードの要点を先ほど示した利用の流れに沿って示したいと思います。 実際のコードはこちらを参照してください。 Web側ですが、フロントエンドとバックエンドそれぞれの仕事を分担できるように、主にフロントだけのファイルとバックエンドに徹したファイルのそれぞれに分けました。 (例:ログイン情報入力ページとログイン情報の照合プログラム)

経営者側

1.管理用Webページにログイン
まずログインシステムについてですが、ログインのためにあらかじめ登録しておいたstoreIDとパスワードをサーバーにPOSTします。ここで、パスワードをデータベースに直接保存しないことでプライバシー性のようなものを保ち、万が一の流出の際にも被害を最小限に抑えるという意味も込めてデータベースに保存する際にSHA-256でハッシュ化します。素人な考えですが、storeIDとパスワードの文字列を結合し、それをさらにハッシュ化することでパスワードのみのハッシュ値よりもハッシュ値の衝突を回避でき、データベースを不正に見られてもパスワードを盗み見られる可能性も減らせると思いました。

〜〜
if(isset($storeID) && isset($password)){
  $_SESSION['storeID'] = $storeID;
  $_SESSION['password'] = $password;
  $_SESSION['keyhash'] = $keyhash;

  //ログインプログラムを走らせる
  header("location: user/login.php");
  exit();
〜〜

storeIDとパスワードとハッシュ値を受け取った後には、受け取ったIDがデータベースに存在するかデータベースに問い合わせるプログラムを呼び出します。受け取ったstoreIDとハッシュ値をもとに、データベースに指定の組み合わせのデータが存在するか確認するためにクエリを叩きます。きちんと照合がとれたらセッションをスタートし、店舗向けのトップページに飛ばします。

送信先はこのようなプログラムになっています。

////////////////////////////
//ログイン状況を取得
////////////////////////////
  //index.phpからstoreIDとパスワードを取得
  $storeID = $_SESSION['storeID'];
  $password = $_SESSION['password'];
  $keyhash = $_SESSION['keyhash'];

  //IDとパスワードが入力されてなかったら
  if($storeID == "" || $password == ""){
    $_SESSION['Error_str'] = "Form not enough";
    header("location: ../index.php");
    exit();
  }

  //データベース接続開始
  $adress = 'xxxxxxxxx';
  $dbname = 'xxxxxxxxx';
  $charset = 'utf8';
  $forceUser = 'xxxxxxxx';
  $password = 'xxxxxxxx';

  $pdostring = 'mysql:host='.$adress.';dbname='.$dbname.';charset='.$charset;

  //pdoオブジェクトを作成
  try{
    $kokokuruDB = new PDO($pdostring, $forceUser, $password);
    $kokokuruDB -> setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
  }catch(PDOException $e){
    exit('データベース接続失敗...'.$e->getMessage());
  }

  //storeIDの存在を探すために値を取り出してみる
  //同じIDを探すクエリ発行
  $query = 'SELECT * FROM `storeList` WHERE `storeID` LIKE :storeID';
  $standby = $kokokuruDB -> prepare($query);
  $standby -> bindParam(':storeID', $storeID);
  $standby -> execute();
  //ここでselectして出てきた結果を配列として格納してる
  $result = $standby -> fetch(PDO::FETCH_ASSOC);
  //今回欲しいのはstoreIDの結果なので$result[storeID]の結果を代入する
  //なかったらboolean型のFALSEが入ってます
  $targetID = $result[storeID];
〜〜〜

最終的に出てきた$targetIDtrueなら$_SESSIONに店舗の広告情報や来客者情報を代入したあとに店舗のタイトルページにジャンプし、falseなら再度ログイン情報を入力するページにジャンプする処理を書きました。

2.設定中の広告情報と来客情報を取得してページを表示
設定中の広告情報ですが、先ほどのログイン処理で$_SESSIONに保存した内容を動的にHTMLに反映しています。来客情報も同様に、先ほどのログイン処理でサインアップ時に自動で作成される各店舗のテーブルにアクセスし、来客情報を取り出してあります。 この来客情報を表示するグラフですが、GoogleChartAPIを利用できるライブラリが公開されていましたので利用させていただきました。

  //外部ライブラリ
  include('GoogleCharts.class.php');
  $GoogleCharts = new GoogleCharts;

  //セッション開始
  session_start();
  //外部ライブラリ
  //include('GoogChart.class.php');
/////////////////////////////////////
//ログイン成功後に表示されるタイトルページ
////////////////////////////////////

  //ログアウトボタンが押された時の処理
  if(isset($_POST["logout"])){
    header("location: logout.php");
    exit();
  }
  //広告編集のボタンが押された時の処理
  if(isset($_POST["adManager"])){
    header("location: adManageForm.php");
    exit();
  }
  //来客情報更新のボタンが押された時の処理
  if(isset($_POST["visitorData"])){
    $data = array(
    Array('時間', '男性', '女性'),
    Array('10時',  serchFromVisitorMen(10, $_SESSION['USERDATA']['ID']),serchFromVisitorWomen(10, $_SESSION['USERDATA']['ID'])),
  Array('11時',  serchFromVisitorMen(11, $_SESSION['USERDATA']['ID']),serchFromVisitorWomen(11, $_SESSION['USERDATA']['ID'])),
    Array('12時',  serchFromVisitorMen(12, $_SESSION['USERDATA']['ID']),serchFromVisitorWomen(12, $_SESSION['USERDATA']['ID'])),
    Array('13時',  serchFromVisitorMen(13, $_SESSION['USERDATA']['ID']),serchFromVisitorWomen(13, $_SESSION['USERDATA']['ID'])),
    Array('14時',  serchFromVisitorMen(14, $_SESSION['USERDATA']['ID']),serchFromVisitorWomen(14, $_SESSION['USERDATA']['ID'])),
    Array('15時',  serchFromVisitorMen(15, $_SESSION['USERDATA']['ID']),serchFromVisitorWomen(15, $_SESSION['USERDATA']['ID'])),
    Array('16時',  serchFromVisitorMen(16, $_SESSION['USERDATA']['ID']),serchFromVisitorWomen(16, $_SESSION['USERDATA']['ID'])),
    Array('17時',  serchFromVisitorMen(17, $_SESSION['USERDATA']['ID']),serchFromVisitorWomen(17, $_SESSION['USERDATA']['ID'])),
    Array('18時',  serchFromVisitorMen(18, $_SESSION['USERDATA']['ID']),serchFromVisitorWomen(18, $_SESSION['USERDATA']['ID'])),
    Array('19時',  serchFromVisitorMen(19, $_SESSION['USERDATA']['ID']),serchFromVisitorWomen(19, $_SESSION['USERDATA']['ID'])),
    Array('20時',  serchFromVisitorMen(20, $_SESSION['USERDATA']['ID']),serchFromVisitorWomen(20, $_SESSION['USERDATA']['ID'])),
);

/**
*   OPTIONS
*/
$options = Array(
    'title' => '本日の来客数',
    'vAxis' => Array('title' => "人数"),
    'hAxis' => Array('title' => "曜日"),
    'seriesType' =>"bars",
    //↓この数字をいじると、該当番目のグラフが指定形式になる。
    //無くしたら全部普通の棒グラフ
    //'series' => Array( 1 => Array( 'type' => "line")),
);



/**
*   CHART
*/
$chart = $GoogleCharts->load( 'combo' , 'chart_div' )->get( $data , $options );

    //グラフデータを更新してからタイトルページに飛ばす
    $_SESSION['USERDATA']['VISITOR_CHART'] = $chart;
    header("location: titlepage.php");
    exit();
  }

  //////////////////////
  //関数定義
  //////////////////////

  //検索条件から、その条件の人数を返却する関数
  //$serchTerms:検索時間(24時間表記0〜24の整数)
  //$visitorSex:検索対象性別("men","women","both")
  function serchFromVisitorMen($serchTime, $targetID){
    //データベース接続開始
    $adress = 'xxxxxxxxx';
    $dbname = 'xxxxxxxxx';
    $charset = 'utf8';
    $forceUser = 'xxxxxxxx';
    $password = 'xxxxxxxx';

    //pdoオブジェクトに入れるときは
    //$pdo = new pdo($pdostring, $forceUser, $password);って感じでお願いします。
    $pdostring = 'mysql:host='.$adress.';dbname='.$dbname.';charset='.$charset;

    //pdoオブジェクトを作成
    try{
      $kokokuruDB = new PDO($pdostring, $forceUser, $password);
      $kokokuruDB -> setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    }catch(PDOException $e){
      exit('データベース接続失敗...'.$e->getMessage());
    }
    $tableName = "visitor_" . $targetID;
    $beforeTime = date("Y-m-d ").$serchTime.":00:00";
    $afterTime = date("Y-m-d ").$serchTime .":59:59";
    $query = "SELECT * FROM `kokokuru`.`".$tableName."` WHERE `visited_at` BETWEEN '".$beforeTime."' AND '".$afterTime."'". "AND `visitorSex` = \"男性\"";
    //データ数を格納する変数
      $resultSumDataNum = 0;
    //来客者数をカウント
    foreach ($kokokuruDB->query($query) as $row) {
      $resultSumDataNum++ ;
    };
    return $resultSumDataNum;

  }

  function serchFromVisitorWomen($serchTime, $targetID){
    //データベース接続開始
    $adress = 'xxxxxxxxx';
    $dbname = 'xxxxxxxxx';
    $charset = 'utf8';
    $forceUser = 'xxxxxxxx';
    $password = 'xxxxxxxx';

    //pdoオブジェクトに入れるときは
    //$pdo = new pdo($pdostring, $forceUser, $password);って感じでお願いします。
    $pdostring = 'mysql:host='.$adress.';dbname='.$dbname.';charset='.$charset;

    //pdoオブジェクトを作成
    try{
      $kokokuruDB = new PDO($pdostring, $forceUser, $password);
      $kokokuruDB -> setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    }catch(PDOException $e){
      exit('データベース接続失敗...'.$e->getMessage());
    }
    $tableName = "visitor_" . $targetID;
    $beforeTime = date("Y-m-d ").$serchTime.":00:00";
    $afterTime = date("Y-m-d ").$serchTime .":59:59";
    $query = "SELECT * FROM `kokokuru`.`".$tableName."` WHERE `visited_at` BETWEEN '".$beforeTime."' AND '".$afterTime."'". "AND `visitorSex` = \"女性\"";
    //データ数を格納する変数
      $resultSumDataNum = 0;
    //来客者数をカウント
    foreach ($kokokuruDB->query($query) as $row) {
      $resultSumDataNum++ ;
    };
    return $resultSumDataNum;

  }

このように書くことで1日の1時間毎の性別ごとの来客人数を表示しています。 GoogleChartAPIを公開してくださった方のおかげでJavaScriptを知らなくてもグラフを表示できました。この場でお礼をさせていただきます。ありがとうございます。 広告情報についてはログイン時に情報を格納した$_SESSIONから広告のタイトル、本文、画像のパスを取得し、HTMLに反映させています。

3.広告情報の登録
続いて広告情報の編集についてですが、 これは広告情報を登録するプログラムに広告のタイトル・本文・画像をPOSTし、それぞれ店舗自体の情報を格納するデータベースに保存しています。

<form action="adManager.php" method="post" enctype="multipart/form-data" class="form-input-ad-style">
  <br>
  <label class="label-adtitle-style">タイトル</label>
  <input type="text" name="adTitle" value="" class="input-adtitle-style">
  <br>
  <label class="label-adbody-style">本文</label>
  <textarea type="text" name="adBody" class="textarea-adbody-style"></textarea>
  <br>
  <input type="file" name="upfile" size="7000000" id="upload" class="input-upfile-style">
  <br>
  <input type='submit' name="send" value="更新" class="input-submit-style">
</form>

このようにPOSTしたものを以下のプログラムで受け取り、データベースの広告情報を更新します。

//////////////////////////////////////////////
//入力された広告情報をデータベースに登録するプログラム
//////////////////////////////////////////////

//データベース接続開始
$adress = 'xxxxxxxxx';
$dbname = 'xxxxxxxxx';
$charset = 'utf8';
$forceUser = 'xxxxxxxx';
$password = 'xxxxxxxx';

//pdoオブジェクトに入れるときは
//$pdo = new pdo($pdostring, $forceUser, $password);って感じでお願いします。
$pdostring = 'mysql:host='.$adress.';dbname='.$dbname.';charset='.$charset;
//pdoオブジェクトを作成
try{
    $kokokuruDB = new PDO($pdostring, $forceUser, $password);
    $kokokuruDB -> setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}catch(PDOException $e){
    exit('データベース接続失敗...'.$e->getMessage());
}

//次にアップされたファイルの拡張子を取得
$ext = substr($_FILES["upfile"]["name"], strrpos($_FILES["upfile"]["name"], '.') + 1);
//ファイル名(_[storeID]_[His].[拡張子])とする
if($ext == "" || $ext == null){
  $filename = "";
}else{
  unlink("../img/".$_SESSION['USERDATA']['PICPASS']);
  $date = date("His");
  $filename = "_".$_SESSION['USERDATA']['ID']."_".$date.".".$ext;

}


//まず最初に画像をサーバーに保存
$picPass = $_SERVER["DOCUMENT_ROOT"]."/cocokuru/img/" .$filename;
if (is_uploaded_file($_FILES["upfile"]["tmp_name"])) {
  //unlink($_SERVER["DOCUMENT_ROOT"]."cocokuru/img/".$_SESSION['USERDATA']['PICPASS']);
    if (move_uploaded_file ($_FILES["upfile"]["tmp_name"], $picPass)) {
    chmod("../img/".$filename, 0777);
    echo $filename . "としてアップロードしました。";
    }else{
    echo "アップロードできませんでした。";
    }
}else{
    echo "ファイルが選択されていません。";
}


//作成者は頭が悪いので何度もクエリを叩く
//titleをUPDATE
//POSTされたタイトルをデータベースに代入
//UPDATEするレコードはstoreNumberで挿入
//多分storeIDでもいけるけどそんなに大差ないから帰る必要はないかと。
$query = 'UPDATE `storeList` SET `adTitle` = \''.$_POST['adTitle'].'\' WHERE `storeList`.`storeNumber` ='.$_SESSION['USERDATA']['NUMBER'];
if($kokokuruDB -> query($query))
{
  $_SESSION['USERDATA']['adTITLE'] = $_POST['adTitle'];
    echo "タイトルOK";
}

//bodyをUPDATE
//POSTされた広告本文をデータベースに挿入
$query = 'UPDATE `storeList` SET `adBody` = \''.$_POST['adBody'].'\' WHERE `storeList`.`storeNumber` ='.$_SESSION['USERDATA']['NUMBER'];
if($kokokuruDB -> query($query))
{
  $_SESSION['USERDATA']['adBODY']  = $_POST['adBody'];
    echo "本文OK";
}

//picNameをUPDATE
$query = 'UPDATE `storeList` SET `picPass` = \''.$filename.'\' WHERE `storeList`.`storeNumber` ='.$_SESSION['USERDATA']['NUMBER'];
if($kokokuruDB -> query($query))
{
  $_SESSION['USERDATA']['PICPASS'] = $filename;
    echo "パスOK";
}

$query = 'UPDATE `storeList` SET `updated_at` = CURRENT_TIMESTAMP WHERE `storeList`.`storeNumber` ='.$_SESSION['USERDATA']['NUMBER'];
if($kokokuruDB -> query($query))
{
  //デバッグ用に表示してますので、実装時には削除しておいたほうがいいです。
  $subquery = 'SELECT * FROM `storeList` WHERE `storeID` LIKE :storeID';
  $standby = $kokokuruDB -> prepare($subquery);
  $standby -> bindParam(':storeID', $_SESSION['USERDATA']['ID']);
  $standby -> execute();
  //ここでselectして出てきた結果を配列として格納してる
  $result = $standby -> fetch(PDO::FETCH_ASSOC);
  //今回欲しいのはkeyhashの結果なので$result[keyhash]の結果を代入する
  //なかったらboolean型のFALSEが入ってます
  $_SESSION['USERDATA']['LAST_UPDATE'] = $result['updated_at'];
    echo "アップデートOK";
}

$_SESSION['STATUS'] = "登録完了";
header("location: adManageForm.php");
exit();

これで広告情報の更新は完了しました。 店舗側の人が操作するページの概要は以上になります。 次は広告を受信するお客様側の処理になります。

お客様側

お客様側が受信するiBeaconの受信はSwift3系を使って処理を書きました。 初回起動時には位置情報の利用の許可やバックグラウンドでの通知の許可を行いますが、それは他にもわかりやすい説明を書いてくださっている方がたくさんいますので今回は割愛し、実際の利用の流れに沿って示していきます。

1.店舗に近づいたら通知を表示
ここではiBeaconのRegionを判別し、指定UUIDのiBeaconを認識したら通知を表示する処理を記述します。

// リージョンを作成.
myBeaconRegion = CLBeaconRegion(proximityUUID: uuid as UUID, identifier: identifierStr)
// ディスプレイがOffでもイベントが通知されるように設定(trueにするとディスプレイがOnの時だけ反応).
myBeaconRegion.notifyEntryStateOnDisplay = false
// 入域通知の設定.
myBeaconRegion.notifyOnEntry = true
// 退域通知の設定.
myBeaconRegion.notifyOnExit = false

このようにリージョンを設定することで、バックグラウンドで位置情報の取得を許可していれば画面が消えている時でも受信可能領域の出入を判別することが可能になります。 今回は領域に入った場合のみ通知を表示するため、 myBeaconRegion.notifyOnEntry = true myBeaconRegion.notifyOnExit = false としています。 もともと領域に入った段階ですぐにバックグラウンドでiBeaconのmajorとminorを判別し、ローカル通知の内容を店舗の広告にしたかったのですがうまくいかず、公式リファレンスを参照したところ、それが不可能であることがわかりましたのでこのような形となりました。

2.受信可能エリア内でアプリを開いたら、店舗のiBeaconの情報を取得
先ほどの通知が表示されるエリア内でアプリをフォアグラウンドに移行させるとiBeaconの詳細情報を受信するメソッドであるlocationManager(_ manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], in region: CLBeaconRegion)が呼び出されます。これは領域内かどうかは関係なく1秒ごとに呼び出されるそうです。このメソッドによってiBeaconのmajorminor、さらには大まかな距離を判別できるため、距離に応じて入店か判断し、同時に来店者としてカウントするかも判断しています。

//iBeaconを検出していなくても1秒ごとに呼ばれる.
    func locationManager(_ manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], in region: CLBeaconRegion) {
        
        // 配列をリセット
        beaconUuids = NSMutableArray()
        beaconDetails = NSMutableArray()
        
        // 範囲内で検知されたビーコンはこのbeaconsにCLBeaconオブジェクトとして格納される
        // rangingが開始されると1秒毎に呼ばれるため、beaconがある場合のみ処理をするようにすること.
        if(beacons.count > 0){
            // 発見したBeaconの数だけLoopをまわす
            for i in 0 ..< beacons.count {
                
                let beacon = beacons[i]
                
                let beaconUUID = beacon.proximityUUID;
                let minorID = beacon.minor;
                let majorID = beacon.major;
                let rssi = beacon.rssi;
                
                //print("UUID: \(beaconUUID.UUIDString) minorID: \(minorID) majorID: \(majorID)");
                
                var proximity = ""
                
                switch (beacon.proximity) {
                    
                case CLProximity.unknown :
                    print("Proximity: Unknown");
                    proximity = "Unknown"
                    //ラベルをリセット
                    statusLabelReset()
                    break
                    
                case CLProximity.far:
                    print("Proximity: Far");
                    proximity = "Far"
                    //サーバーにデータを送信
                    sendVisitData(
                        majorID: CLBeaconMajorValue(truncating: majorID),
                        minorID: CLBeaconMinorValue(truncating: minorID),
                        proximity: proximity)
                    
                    break
                    
                case CLProximity.near:
                    print("Proximity: Near");
                    proximity = "Near"
                    //サーバーにデータを送信
                    sendVisitData(
                        majorID: CLBeaconMajorValue(truncating: majorID),
                        minorID: CLBeaconMinorValue(truncating: minorID),
                        proximity: proximity)
                    
                    break
                    
                case CLProximity.immediate:
                    print("Proximity: Immediate");
                    proximity = "Immediate"
                    //サーバーにデータを送信
                    sendVisitData(
                        majorID: CLBeaconMajorValue(truncating: majorID),
                        minorID: CLBeaconMinorValue(truncating: minorID),
                        proximity: proximity)
                    
                    break
                }
                
                beaconUuids.add(beaconUUID.uuidString)
                
                var myBeaconDetails = "Major: \(majorID) "
                myBeaconDetails += "Minor: \(minorID) "
                myBeaconDetails += "Proximity:\(proximity) "
                myBeaconDetails += "RSSI:\(rssi)"
                print(myBeaconDetails)
                beaconDetails.add(myBeaconDetails)
            }
        }
    }

これでiBeaconの情報は取得できました。

3.取得した情報からPHPで作成したAPIに来客者の情報が入ったJSONをPOSTする
4.APIから店舗の広告情報をJSONで受け取る
iBeaconからmajorminorを取得できたため、この情報から店舗を特定して広告データを受信したいと思います。 iBeaconの情報を取得出来次第、作成したAPIJSON形式でmajorminorユーザー情報の3つのデータを送信します。 作成したJSONのイメージはこんな感じです。

//店舗特定のために送信するJSON
{
  "regist":"yes"または"no" //<-登録するかの判断
  "majorID":majorID
  "minorID":minorID
  "userData":{
               "sex": 'アプリユーザーの性別'
               "birthYear": '生まれた年'
               "interest":'趣味・興味'
  }
}

この性別``生まれた年``趣味・興味はアプリ初回起動時にチュートリアルで入力してもらい、UserDefaultに保存しておいてある情報からのものです。この情報を作成したAPIにPOSTし、広告情報の取得と来客情報の登録をAPIにやってもらいます。 作成したAPIはこのようになっています。

////////////////////////////////
//iBeaconを受信したあと、スマートフォンから、
//JSON形式で情報を受け取ったのち、①
//MySQLと照合して、あってる店があったら②
//対象の店の来店情報に登録して、④
//広告情報を返すプログラム③
////////////////////////////////////


//まずJSONを受けとって配列に変換する
$json = file_get_contents('php://input');
$json = mb_convert_encoding($json, 'UTF8', 'ASCII,JIS,UTF-8,EUC-JP,SJIS-WIN');
$visitorData = json_decode($json, true);

//次にデータベースに接続し、受信したmajorとminorと同じ組み合わせを持つ店を検索する。
//データベース接続開始
//ここはそれぞれのデータベース参照してください。
$adress = 'xxxxxxx';
$dbname = 'xxxxxx';
$charset = 'utf8';
$forceUser = 'xxxxxxx';
$password = 'xxxxxxxx';

//pdoオブジェクトに入れるときは
//$pdo = new pdo($pdostring, $forceUser, $password);って感じでお願いします。
$pdostring = 'mysql:host='.$adress.';dbname='.$dbname.';charset='.$charset;

//pdoオブジェクトを作成
try{
  $kokokuruDB = new PDO($pdostring, $forceUser, $password);
  $kokokuruDB -> setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}catch(PDOException $e){
  exit('データベース接続失敗...'.$e->getMessage());
}

//storeIDの存在を探すために値を取り出してみる
//同じIDを探すクエリ発行
$query = 'SELECT * FROM `storeList` WHERE `major` LIKE :majorID AND `minor` LIKE :minorID';
$standby = $kokokuruDB -> prepare($query);
$standby -> bindParam(':majorID', $visitorData['majorID']);
$standby -> bindParam(':minorID', $visitorData['minorID']);
$standby -> execute();
//ここでselectして出てきた結果を配列として格納してる
//なかったらFALSEが帰ってくる
$result = $standby -> fetch(PDO::FETCH_ASSOC);

//見つかったら、広告内容を変数に代入
if($result){
  //返すJSONを作成する。
  $setJSON = array(
    "status" => "success",
    "storeID" => $result['storeID'],
    "storeName" => $result['storeName'],
    "adTitle" => $result['adTitle'],
    "adBody" => $result['adBody'],
    "picPass" => $result['picPass'],
    "updated_at" => $result['updated_at']
  );
}else{
  //見つからなかったらステータスに失敗を代入
  $setJSON = array(
    "status" => "fail",
    "storeID" => "fail",
    "storeName" => "fail",
    "adTitle" => "fail",
    "adBody" => "fail",
    "picPass" => "fail",
    "updated_at" => "fail"
  );
}


//作成した配列データをJSON文字列にして呼び出し元に返す
$returnJSON = json_encode($setJSON, JSON_UNESCAPED_UNICODE);
echo $returnJSON;


//次に登録指示があったら、来店情報を各店舗の来客データベースに登録する。
//データベースオブジェクトから店舗IDを取得
if ($visitorData['regist'] == "yes"){
  $targetID = $result['storeID'];

  //挿入先のテーブル名
  $targetTable = "visitor_".$targetID;
  //来客情報データベースに来客情報を登録する
  //データベースにinsertするクエリの生成
  $query = 'INSERT INTO `kokokuru`.`'.$targetTable.'` (`visitorBirthYear`, `visitorSex`, `interest1`, `visited_at`
                                                   ) VALUES (
                                                    \''.$visitorData['userData']['birthYear'].'\',
                                                    \''.$visitorData['userData']['sex'].'\',
                                                    \''.$visitorData['userData']['interest'].'\',
                                                    CURRENT_TIMESTAMP
                                                     );';
  //クエリ実行
  $kokokuruDB -> query($query);
}

思ったよりも短い量で済みました。

Swift上ではこのようにJSONを作成・POSTしました。

//JSONを作って指定サーバーのプログラムに送信
    func sendVisitData(majorID: CLBeaconMajorValue, minorID:CLBeaconMinorValue, proximity:String){
        print("Begin visitng data send...");
        print("MajorID: \(majorID)  MinorID: \(minorID)");

        //ユーザーデータのオブジェクトを作成
        var userData = Dictionary<String,Any>()
        userData["sex"] = self.delegate.userDefault.string(forKey: "sex")
        userData["birthYear"] = self.delegate.userDefault.string(forKey: "birthYear")
        userData["interest"] = self.delegate.userDefault.string(forKey: "interest")
        //送信するJSON(Dictionaly型)を作成
        var jsonDict = Dictionary<String,Any>()
        jsonDict["regist"] = judgeResist(proximity: proximity)         //登録するならyesしないならno
        jsonDict["majorID"] = majorID
        jsonDict["minorID"] = minorID
        jsonDict["userData"] = userData
        
        //http通信のリクエストを作成する
        let urlString = "http://aoking21.softether.net/cocokuru/api/visitorDataReceiver.php"
        let request = NSMutableURLRequest(url: URL(string: urlString)!)
        
        request.httpMethod = "POST"
        request.addValue("application/json", forHTTPHeaderField: "Content-type")
        
        do{
            request.httpBody = try JSONSerialization.data(withJSONObject: jsonDict, options: [])
            
            let task:URLSessionDataTask = URLSession.shared.dataTask(with: request as URLRequest, completionHandler: {(data,response,error) -> Void in
                let resultData = String(data: data!, encoding: .utf8)
                // dataをJSONパースし、グローバル変数"getJson"に格納
                if(resultData?.isEmpty == false){
                    self.lastGetJSON = self.getJSON
                    self.getJSON = resultData
                    print("result:\(self.getJSON)")
                }
            })
            
            task.resume()
     ~~~~~~~~~       

このタスクのレスポンスとして、店舗の広告情報が先ほどのAPIからJSON形式で返却されてきます。

5.JSONから店舗の広告情報を取得して表示する
ではレスポンスで帰ってきたJSONの中身ですが以下のようになっています。

{
   "status":"success" または "fail"
    "adTitle" : "広告のタイトル"
    "adBody" : "広告の本文"
    "picPass" : "広告の画像の名前"
    "updated_at" : "広告の更新日
    "storeID" : ストア識別ID
    }
}

このJSONをパースする際には以下のように処理しました。

/*
//JSONをパースしてグローバル変数に代入するメソッド
 引数: jsonStr -> JSON形式の文字列
*/
func jsonPerse(jsonStr: String){
    do {
        let jsonData: Data = jsonStr.data(using: String.Encoding.utf8)!
        // JSONパース
        let json = try JSONSerialization.jsonObject(with: jsonData as Data, options: JSONSerialization.ReadingOptions.allowFragments) as! NSDictionary
        self.statusFromAPI = (json["status"] as? String)!
        self.storeName = (json["storeName"] as? String)!
        self.adTitle = (json["adTitle"] as? String)!
        self.adBody = (json["adBody"] as? String)!
        self.picPass = (json["picPass"] as? String)!
        self.updatedAt = (json["updated_at"] as? String)!
        //print(self.adTitle)
    } catch {
        print(error) // パースに失敗したときにエラーを表示
           
    }
}

この関数をレスポンスを受け取った直後に呼び出すことでグローバル変数に代入しています。 この受け取ったJSONの中には写真のファイル名もありますので、これを画像を取得しViewに適用するための関数に渡すことで画像を表示しました。関数は以下のようになっています。

/*
//画像を取得する関数
  picPass -> APIから受け取った写真のファイル名
 */
func getAdPicture(picPass: String){
    let picUrlString: String = "http://aoking21.softether.net/cocokuru/img/"+picPass;
    let picURL = URL(string: picUrlString)
    let imageData = try? Data(contentsOf: picURL!)
    let image = UIImage(data: imageData!)
    self.adPictureView.contentMode = UIViewContentMode.scaleAspectFit
    self.adPictureView.image = image
}

以上でユーザーが使用するアプリの流れは終わりとなります。 その他全体のコードGithubにありますのでぜひご覧ください。

工夫した点

  • 今までiBeaconを使った広告配信は存在したが、設置店舗が簡単に広告を変更できるようにWebで全ての操作を完結できるようにした。
  • 広告の送信だけではなく、広告配信によって新しい経営戦略につながるように受信したユーザーを視覚的にわかりやすく表示できるようにグラフを使用した。
  • 写真をJSON形式で受信する方法がわからなかったため、画像のファイル名だけを取得し、URLに当てはめることで画像の取得を実装した。

改善点

  • レスポンシブデザインに対応させ、スマートフォン上で広告の配信と来客情報の確認をできるようにしたい。
  • デザインがわかりにくい部分が多い
  • 1秒ごとに通信するため、電池の消費が多そう(未実証)
  • ソースが汚い

参考にさせていただいたサイト

以下は参考にさせていただいたサイト様です。 私よりもわかりやすく説明されていますのでご確認ください。 大変お世話になりました。

とりあえずご挨拶

このブログについて

こんにちは。こんばんはかもしれません。
ぼちぼちブログを書いていこうと思っています、aoking21です。

このブログについてですが、今まで自分がやってきた勉強もを忘れないようにするためにも書き始めました。
内容的にはWebとかアプリとかRasberry Piの技術について書いていくつもりです。
一言でいうなら備忘録ってやつですね。

趣味についても書くかもしれません。

PC環境

一応参考程度にブログ開設時点でのPCについても触れておきます。
メインで使うのは

以上の4つですね。
最近はMacBook Proしか使っていないので、だいたいmac絡みをアップする予定です。

次回

次回は最近作成したiBeaconを使った広告配信システムについて書く予定です。
こんな感じで語彙力が足りない人ですがよろしくお願いします。