Physical Webごっこ

# Physical Web

[Physical Web](https://google.github.io/physical-web/)という言葉を聞いたことがあるでしょうか？Physical WebはGoogleが提唱している、簡単に言えば**物理的なモノにURLを付与するための規格**です。具体的には[Eddystone](https://github.com/google/eddystone)という規格を実装したビーコンが機器に対応するURLを定期的に周囲に発信し、そのビーコンにスマホのウェブブラウザ（現在はChromeのみ）が反応して通知を表示するようにすることでモノのURLを実現します。

# Physical Webパーキングメーター

今年の[Chrome Dev Summit](https://developer.chrome.com/devsummit)の会場にはPhysical Webを使用したデモがいくつか設置されていました。その1つがPhysical Webパーキングメーターです。そのデモでは「駐車枠に車を止めてパーキングメーターに近づくとスマホに通知が届き、そのままスマホからパーキングメーターの操作や支払いができる」というシナリオを実際に体験することができました。（ただし"駐車枠に車を止めて"の部分は脳内で補完する必要があります）

[![Physical Web Parking Meter](http://img.youtube.com/vi/ysxB_PXFImE/0.jpg)](https://www.youtube.com/watch?v=ysxB_PXFImE)

[Chrome Dev Summit](https://developer.chrome.com/devsummit)のおみやげとしてBeaconを頂いたので、今回はこのPhysical Webパーキングメーターもどきを作ってみます。

# Physical Webパーキングメーターもどき

## パーキングメーターウェブアプリ作成

ラップトップで動作するパーキングメーターっぽいウェブアプリと、スマホで動作するパーキングメータークライアントっぽいアプリを作成します。クライアントからパーキングメーターを操作できればなんでもよく、普通のウェブアプリでPhysical Webの仕様には関係ない上、作りも適当なので説明は省略します。一応ソースコードは一番に下においておきます。

## ビーコンを設定

ちなみにビーコンはこの真ん中の白い奴です。

<img width="400px" alt="IMG_20151226_013204.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/8717/2f226a27-4ac3-0d70-fee4-3e2e676a250d.jpeg">

ビーコンに近付いたらスマホでパーキングメータークライアントが開くようにするために、まずビーコンにそのURLを設定しなければいけません。Androidに[BEEKS Beacon Maker](https://play.google.com/store/apps/details?id=com.bluvision.beaconmaker)というビーコンを設定するためのアプリをインストールして起動します。

<img width="300px" alt="Screenshot_20151226-010822.png" src="https://qiita-image-store.s3.amazonaws.com/0/8717/a6152d0c-cbc4-29d1-7e0f-7d9ba2e07cf9.png">

スマホをビーコンに近づけてしばらく待つと緑のアイコンが出てくるのでクリックします。

<img width="300px" alt="Screenshot_20151226-010946.png" src="https://qiita-image-store.s3.amazonaws.com/0/8717/4c73c6ef-ca2f-beb1-588d-830967ff9bb2.png">

右上のCONNECTボタンをクリックしてビーコンに接続して

<img width="300px" alt="Screenshot_20151226-012321.png" src="https://qiita-image-store.s3.amazonaws.com/0/8717/f10b3db1-718d-c2f6-9a4d-c360aeb91733.png">

CONF.EDDYSTONEボタンをクリックして設定ダイアログを開き、Physical Webパーキングメータークライアントもどきの短縮URLを入力します。ビーコンに設定できるURLのサイズは非常に短いので基本的に何らかの短縮URLサービスを利用する必要があります。

- パーキングメータークライアントもどき: https://physical-web-modoki.firebaseapp.com/index.html#<パーキングメーターID>
- 今回使用した短縮URL: https://goo.gl/VO8x9l

<img width="300px" alt="Screenshot_20151226-012118.png" src="https://qiita-image-store.s3.amazonaws.com/0/8717/0dc63f46-57ed-eb2d-8c0f-ae6e7b98f415.png">

はじめてビーコンを使用する場合はCONF.ADVERTISEMENTSボタンもクリックして電波の強度やURLを発信する周期を設定しておきます。とりあえず半径12メートル程度のエリアに2回／秒ほど発信するよう設定しました。

これでビーコンの設定は終了です。

## ビーコンを受信

あとはChrome開発者版でPhysical Webフラグを有効にしてスマホをビーコンに近づければ何かいい感じのことが起こるはずですが、なぜかうまくいかないのでひとまずここは[Physical Webアプリ](https://play.google.com/store/apps/details?id=physical_web.org.physicalweb)をインストールしておきます。

# デモ

## 準備

- パーキングメーターもどき: https://physical-web-modoki.firebaseapp.com/meter.html

ラップトップでパーキングメーターもどきアプリを立ち上げて近くにビーコンを置き、スマホではPhysical Webを立ち上げておきます。

<img width="300px" alt="IMG_20151227_001456.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/8717/bea8a3eb-8b2b-5771-6f96-c717d20579cf.jpeg">

これが我が家のパーキングメーター。

## ごっこ遊び

（脳内で）自動車を駐車して、ポケットにスマホを入れたまま、パーキングメーター（ビーコンを近くにおいたラップトップ）に近づきます。

<img width="300px" alt="Screenshot_20151226-012522.png" src="https://qiita-image-store.s3.amazonaws.com/0/8717/8cc50efc-6860-4554-2213-a2cc40f9a95a.png">

Physical Webアプリ（本来であればブラウザ）がビーコンを受信してリンクが表示されました。

<img width="300px" alt="Screenshot_20151226-012530.png" src="https://qiita-image-store.s3.amazonaws.com/0/8717/bb6a8ea3-226a-48c0-5a4d-2b5a57c2ee63.png">

リンクをクリックするとブラウザが開き、Physical Webパーキングメータークライアントもどきが立ち上がります。

・・・

本来はこのままスマホでスマホでクライアントアプリを開いてラップトップ側のパーキングメーターを操作する想定ですが、説明が面倒くさかったので、両方をラップトップで起動して操作しているところをスマホで撮影しました。ここから先は左側の画面はスマホだと思って動画を見てください。

・・・

[![Physical Webもどき](http://img.youtube.com/vi/dsVpYLMfwa8/0.jpg)](https://www.youtube.com/watch?v=dsVpYLMfwa8)

ということで、自動車を駐車してパーキングメーターに近づくとパーキングメータークライアントもどきが立ち上がり、パーキングメーターもどきをスマホから操作するというごっこ遊びができるようになりました。

おしまい。

# ソースコード

## パーキングメーター

```lang:html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <title>Physical Webパーキングメータもどき</title>
    <style>
      * {
        margin: 0;
        padding: 0;
      }
      h1 {
        font-size: 120%;
        text-transform: uppercase;
        color: white;
        text-align: center;
        padding-top: 2vh;
        height: 10vh;
      }
      #timer {
        color: white;
        text-align: center;
        height: 30vh;
        padding-top: 5vh;
        font-size: 20vh;
      }
      #screen {
        width: 100vh;
        height: 100vh;
        border-radius: 50vh;
        background-color: black;
        overflow: hidden;
      }
      #status {
        width: 100vh;
        height: 20vh;
      }
      #status.init {
        background: lightGreen;
      }
      #status.paid {
        background: lightGreen;
      }
      #status.parking {
        background: red;
      }
      #reset {
        position: absolute;
        bottom: 5vh;
        left: 40vh;
        width: 20vh;
        height: 20vh;
        border-radius: 10vh;
        background-color: white;
        border: none;
        text-transform: uppercase;
        font-size: 150%;
      }
    </style>
  </head>
  <body>
    <div id="screen">
      <h1>Meter #<span id="meter-id"></span></h1>
      <div id="timer"></div>
      <div id="status" class="init"></div>
      <button id="reset">Reset</button>
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.0.0-alpha1/jquery.min.js"></script>
    <script src="https://cdn.firebase.com/js/client/2.3.2/firebase.js"></script>
    <script>
      var firebaseRef = new Firebase("https://physical-web-modoki.firebaseio.com");

      var meterId = location.hash.replace('#', '');
      if (!meterId) {
        meterId = Math.floor(Math.random() * 1000);
        location.hash = '' + meterId;
      }
      $('#meter-id').text(meterId);

      var meterRef = firebaseRef.child('meter-' + meterId);

      meterRef.child('timer').on('value', function(snapshot) {
        var timer = snapshot.val() || '00:00';
        $('#timer').text(timer);
      });

      var timerId;
      meterRef.child('status').on('value', function(snapshot) {
        stat = snapshot.val();
        console.log(stat);
        console.dir(stat);
        if (stat.name === 'init') {
          $('#status').addClass('init');
          $('#status').removeClass('paid');
          $('#status').removeClass('parking');
        }
        else if (stat.name === 'paid') {
          $('#status').addClass('paid');
          $('#status').removeClass('init');
          $('#status').removeClass('parking');
          setTimeout(function() {
            meterRef.child('status').set({
              name: 'parking',
              user: stat.user,
              start: +new Date
            });
          });
        }
        else if (stat.name === 'parking') {
          $('#status').addClass('parking');
          $('#status').removeClass('init');
          $('#status').removeClass('paid');
          var timeParts = $('#timer').text().split(':').map(function(v) {
            return parseInt(v, 10);
          });
          var maxTime = (timeParts[0] * 60 + timeParts[1]) * 1000;
          var startTime = stat.start;
          function timer() {
            var currentTime = +new Date
            var diffTime = currentTime - startTime;
            var remainingTime = Math.floor((maxTime - diffTime) / 1000);
            if (remainingTime <= 0) {
              reset();
            }
            else {
              var min = Math.floor(remainingTime / 60);
              var sec = remainingTime - min * 60;
              min = (min < 10 ? '0' : '') + min;
              sec = (sec < 10 ? '0' : '') + sec;
              meterRef.child('timer').set(min + ':' + sec);
              timerId = setTimeout(timer, 1000);
            }
          }
          timer();
        }
      });

      function reset() {
        if (timerId) clearTimeout(timerId);
        meterRef.set({
          name: 'Test Parking #' + meterId,
          status: {name: 'init'},
          timer: '00:00'
        });
      }

      meterRef.once('value', function(snapshot) {
        if (!snapshot.val()) {
          reset();
        }
      });

      $('#reset').click(reset);
    </script>
  </body>
</html>
```

## クライアント

```lang:JavaScript
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
    <title>Physical Webパーキングメータもどき</title>
    <style>
      * {
        margin: 0;
        padding: 0;
      }
      button {
        border: none;
        width: 100%;
      }
      button:active {
        border: none;
        outline: none;
      }
      header {
        padding: 20px;
        font-size: 250%;
        color: gray;
        border-bottom: 1px solid gray;
      }
      section {
        text-align: center;
      }
      h1 {
        color: lightGray;
        font-size: 200%;
      }
      #timer {
        font-size: 400%;
      }
      #increase, #decrease {
        background: none;
        font-weight: bold;
        font-size: 350%;
        transform: scale(5, 1);
        color: lightGray;
        margin: 10px;
      }
      #pay {
        background: lightGreen;
        font-weight: bold;
        font-size: 300%;
        color: white;
        padding: 40px;
      }
      #pay.parking {
        background: red;
      }
    </style>
  </head>
  <body>
    <header>
    </header>
    <section>
      <button id="increase">&and;</button>
      <h1>Meter #<span id="meter-id"></span></h1>
      <article id="timer"></article>
      <button id="decrease">&or;</button>
      <button id="pay">Pay<span id="amount"><span></button>
    </section>
    <footer>
    </footer>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.0.0-alpha1/jquery.min.js"></script>
    <script src="https://cdn.firebase.com/js/client/2.3.2/firebase.js"></script>
    <script>
      var username;
      var status;
      var startTime;
      var firebaseRef = new Firebase("https://physical-web-modoki.firebaseio.com");
      var timer;

      var INCREASES = {
        '00:00': '10:00',
        '10:00': '20:00',
        '20:00': '30:00',
        '30:00': '40:00',
        '40:00': '50:00',
        '50:00': '60:00',
        '60:00': '60:00',
      };

      var DECREASES = {
        '00:00': '00:00',
        '10:00': '00:00',
        '20:00': '10:00',
        '30:00': '20:00',
        '40:00': '30:00',
        '50:00': '40:00',
        '60:00': '50:00',
      };

      var AMOUNTS = {
        '00:00': '',
        '10:00': ' $5',
        '20:00': ' $10',
        '30:00': ' $15',
        '40:00': ' $20',
        '50:00': ' $25',
        '60:00': ' $30',
      };

      function setButtonsEnabled(bool) {
        var val = !bool;
        $('#increase').prop('disabled', val);
        $('#decrease').prop('disabled', val);
        $('#pay').prop('disabled', val);
      }

      function start(authData) {
        username = authData.twitter.username;

        var meterId = location.hash.replace('#', '');
        if (!meterId) {
          alert('set meter id');
        }
        $('#meter-id').text(meterId);

        var meterRef = firebaseRef.child('meter-' + meterId);

        meterRef.child('name').on('value', function(snapshot) {
          var name = snapshot.val();
          $('header').text(name);
        });

        meterRef.child('timer').on('value', function(snapshot) {
          timer = snapshot.val() || '00:00';
          $('#timer').text(timer);
        });

        meterRef.child('status').on('value', function(snapshot) {
          stat = snapshot.val() || 'init';
          if (stat.name === 'init') {
            setButtonsEnabled(true);
            $('#pay').removeClass('parking');
            $('#pay').html('Pay<span id="amount"><span>');
          }
          else if (stat.name === 'paid') {
            setButtonsEnabled(false);
          }
          else if (stat.name === 'parking') {
            setButtonsEnabled(false);
            $('#pay').addClass('parking');
            $('#pay').text('Paid');
          }
        });

        $('#increase').click(function() {
          timer = INCREASES[timer];
          meterRef.update({'timer': timer});
          $('#amount').text(AMOUNTS[timer]);
        });

        $('#decrease').click(function() {
          timer = DECREASES[timer];
          meterRef.update({'timer': timer});
          $('#amount').text(AMOUNTS[timer]);
        });

        $('#pay').click(function() {
          meterRef.update({'status': {
            name: 'paid',
            user: username
          }});
        });
      }

      var authData = firebaseRef.getAuth();
      if (!authData) {
        firebaseRef.authWithOAuthPopup("twitter", function(error, authData) {
          if (error) {
            console.log(error);
            alert(error);
          }
          else if (authData) {
            console.log(authData);
            start(authData);
          }
        });
      }
      else {
        start(authData);
      }
    </script>
  </body>
</html>
```