12
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

HTML5Advent Calendar 2015

Day 24

Physical Webごっこ

Last updated at Posted at 2015-12-24

Physical Web

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

Physical Webパーキングメーター

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

Physical Web Parking Meter

Chrome Dev SummitのおみやげとしてBeaconを頂いたので、今回はこのPhysical Webパーキングメーターもどきを作ってみます。

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

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

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

ビーコンを設定

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

IMG_20151226_013204.jpg

ビーコンに近付いたらスマホでパーキングメータークライアントが開くようにするために、まずビーコンにそのURLを設定しなければいけません。AndroidにBEEKS Beacon Makerというビーコンを設定するためのアプリをインストールして起動します。

Screenshot_20151226-010822.png

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

Screenshot_20151226-010946.png

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

Screenshot_20151226-012321.png

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

Screenshot_20151226-012118.png

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

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

ビーコンを受信

あとはChrome開発者版でPhysical Webフラグを有効にしてスマホをビーコンに近づければ何かいい感じのことが起こるはずですが、なぜかうまくいかないのでひとまずここはPhysical Webアプリをインストールしておきます。

デモ

準備

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

IMG_20151227_001456.jpg

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

ごっこ遊び

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

Screenshot_20151226-012522.png

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

Screenshot_20151226-012530.png

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

・・・

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

・・・

Physical Webもどき

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

おしまい。

ソースコード

パーキングメーター

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>

クライアント

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>
12
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?