Help us understand the problem. What is going on with this article?

えっWebRTCって Web Real-Time Christmastreeのことだよね??【前編】

More than 1 year has passed since last update.

こちらはIoTLT Advent Calendar 2018の24日目の記事です。
【後編】は、SkyWay Advent Calendar 2018最終日の記事になります。


みなさまメリークリスマス。
去年のアドベントカレンダーでは初日からひどい記事を書きました。

当Qiita記事がいいねされたら電飾が光りまくるX'mas Internet of Tree をつくる

皆さま、たくさんのいいねのプレゼントありがとうございました。乞食でした。

それから1年たって、ふと気づいたらSkyWay Advent Calendar 2018に(いつのまにか)登録されていました。WebRTCのウの字も知らず、リアルタイムクリスマスツリーだと曲解した僕は、ブラウザ上でキラキラ光るバーチャルなクリスマスツリーの姿を思い浮かべていました。。。

そんなさなか、忘れ去られていたあのクリスマスツリーが、忘年会前の大掃除によって発掘されてしまいました。ツリーよ、祈るべきは神か、サンタか……

というわけで、当記事では以前に作られたかわいそうなクリスマスツリーが、さらなる受難を経てウェブ上で光り輝くリアルタイムなクリスマスツリーになるまでの物語です。
ツリー本体に関する制作記事は、上記のひどい記事をごらんください。

組み込みWebRTCを実装しようとした

茶番イントロ申し訳ありません。
結論から申し上げますと、一旦は断念しました。

fig1.png

こんな感じで、クリスマスツリーをP2Pでリアルタイムに制御したかったのです。
NefryはESP32なので、UDPソケットがさわれるじゃないか。
じゃあなんとなく実装するか…… まあ3日ぐらいで…… そんな軽い気持ちで調べていたら、

詳解 WebRTC

実は WebRTC というのは沢山のプロトコルが絡み合ってできています。
・UDP
・DTLS
・RTP / RTCP
・SRTP / SRTCP
・STUN / TURN / ICE
・SDP


先程のプロトコルの紹介のときに、全てに RFC が割り当てられているのにお気づきかと思います。先ほど紹介したのは基本の RFC だけです。応用となると大変な数があります。


・ICE
 この話をするだけで1日使える位な世界なので、今回は詳しくは話しません。

なめてた🤗ケーキ食べょ😌🎂

実際この構成ができればわりとすごいことですし、時間かけてでもやってみたい気持ちはあったのですが、ネタで実装できるほど簡単ではないし、そんな技術も僕にはありません。。それに、これだとリモートのブラウザからツリーを制御できても、その眩しい御姿を拝謁する方法がありません。
なので、以下のようにしました。

WebRTC + WebBluetooth

fig2.png

これだと、ローカルからクリスマスツリーを制御する信号をリモートに送りつけて、リモートはWebBluetoothで信号をNefryに転送するようにすれば、遠隔でLED制御ができます。あとはそれをUSBカメラでライブストリーミングしてあげれば、ウェブ上でリアルタイムなクリスマスツリーが楽しめそうですね。

というわけで、【前編】はWebBluetoothとNefry BTを使った「IoT編」、【後編】はSkyWayが提供するJavaScript SDKを用いたWebRTCシステムとWebBluetoothを結合する「Webフロントエンド編」で進めていきたいとおもいます!

まずはWebBluetoothでブラウザから制御する

ツリー側

ツリーとネフリー

IMG_20181225_1245568.jpg

毎度おなじみNefry BTを使います。ツリーとNefryの接続に関しては、前述のひどい記事を参照してください。

スケッチ

XmasTree.ino
#include <Nefry.h>

#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>

// UUIDは以下で自分のものを必ず生成して利用のこと
// https://www.uuidgenerator.net/

#define SERVICE_UUID        "2fb65514-1a38-4597-bf63-590e175a262f"
#define CHARACTERISTIC_UUID "1b68902e-da10-40d8-a58f-c68da82c3021"

#define pin1A D0
#define pin1B D1
#define pin2A D2
#define pin2B D3
#define pin3A D4
#define pin3B D5

// LED制御
void led(char id, bool onoff) {
  // GPIO間で電流の方向を切り替えます
  switch(id) {
    case 'A':
      digitalWrite(pin1A, onoff);
      digitalWrite(pin1B, !onoff);
      Nefry.setLed(100, 100, 0);
      break;
    case 'B':
      digitalWrite(pin2A, onoff);
      digitalWrite(pin2B, !onoff);
      Nefry.setLed(0, 100, 100);
      break;
    case 'C':
      digitalWrite(pin3A, onoff);
      digitalWrite(pin3B, !onoff);
      Nefry.setLed(100, 0, 100);
      break;
    default:
      ledOff();
      break;
  }
}

// ツリーのLEDをぜんぶけす
void ledOff() {
  digitalWrite(pin1A, LOW);
  digitalWrite(pin1B, LOW);
  digitalWrite(pin2A, LOW);
  digitalWrite(pin2B, LOW);
  digitalWrite(pin3A, LOW);
  digitalWrite(pin3B, LOW);
  Nefry.setLed(50, 0, 0);
}

// コールバック
// ブラウザ側からwriteValue()されたら実行
class MyCallbacks: public BLECharacteristicCallbacks {
  void onWrite(BLECharacteristic *pCharacteristic) {
    std::string value = pCharacteristic->getValue();

    // シリアルにデバッグ出力(受信した文字列)
    if (value.length() > 0) {
      Nefry.print("Received: ");
      // 1文字ずつ取得
      for (int i = 0; i < value.length(); i++) Nefry.print(value[i]);
    }
    Nefry.println("");

    // 受信した文字列が2文字のときのみコマンドとして実行
    if (value.length() == 2) {
      // i=0 : LEDチェーンのid番号
      // i=1 : LED状態(0/1)
      if (value[1] == '0') {
        led(value[0], false);
      } else {
        led(value[0], true);
      }
    }
  }
};

void setup() {
  Nefry.setLed(0, 0, 100); // 起動直後:本体LED青点灯
  Nefry.disableWifi();
  pinMode(pin1A, OUTPUT);
  pinMode(pin1B, OUTPUT);
  pinMode(pin2A, OUTPUT);
  pinMode(pin2B, OUTPUT);
  pinMode(pin3A, OUTPUT);
  pinMode(pin3B, OUTPUT);

  // BLEデバイスに俺はなる
  BLEDevice::init("NefryBT(X'mas)");
  Nefry.println("NefryBT(X'mas) Started.");

  // サンプルスケッチ: ESP32 BLE Arduino -> BLE_write からの流用です。
  BLEServer *pServer = BLEDevice::createServer();
  BLEService *pService = pServer->createService(SERVICE_UUID);
  BLECharacteristic *pCharacteristic = pService->createCharacteristic(
                                         CHARACTERISTIC_UUID,
                                         BLECharacteristic::PROPERTY_READ |
                                         BLECharacteristic::PROPERTY_WRITE
                                       );
  pCharacteristic->setCallbacks(new MyCallbacks());
  pCharacteristic->setValue("Merry X'mas!");
  pService->start();
  BLEAdvertising *pAdvertising = pServer->getAdvertising();
  pAdvertising->start();

  Nefry.setLed(0, 100, 0); // 初期化終了:本体LED緑点灯
}

void loop() {
  // 待ちループ
  Nefry.ndelay(500);
}

ESP32でBLEを扱うスケッチのうち、セントラルからペリフェラルにデータを書き込むサンプルのスケッチをもとに改造しました。
参考: BLEについて今更調べてみた

ここでは、受信したデータ文字列が2文字のときのみ、ツリーを制御するためのコマンドとしてデータを扱うようにしました。
1文字目はA,B,Cのいずれかとし、3系統あるLEDケーブルのそれぞれを指定します。2文字目は0か1で、電流方向を順方向か逆方向かにします。どちらに流れてもLEDケーブルの半数は点灯しますが、順方向と逆方向で点灯するLEDが異なる(1つおきに光る)ため、0と1を繰り返すと点滅しているように見えます。
上記以外の2文字が送られてきた場合は、すべてのLEDケーブルに対する出力をとめるようにしています。
これらを、BLEのセントラル(Webブラウザ)からデータが来た時に呼び出されるコールバックの中に記述しました。

ブラウザ側

HTMLとJSごちゃまぜで書きました。

ble.html
<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <title>WebBluetooth - NefryBT - X'masTree</title>
        <meta name="description" content="七面鳥たべたい">
        <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
    </head>

    <body>
        <p>ブラウザからWebBluetoothを介してNefryBTに接続されたクリスマスツリーのLEDを制御する</p>
        <button id="btn_start">接続開始</button>
        <hr>
        <button id="btn_A" class="btn_tests" value="A0">A : off</button>
        <button id="btn_B" class="btn_tests" value="B0">B : off</button>
        <button id="btn_C" class="btn_tests" value="C0">C : off</button>
        <button id="btn_all_off" value="X0">全消灯</button>

        <script>
            // サービスとキャラクタリスティックのUUIDはNefryに書き込んだものと同じです
            const CUSTOM_SERVICE_UUID = "2fb65514-1a38-4597-bf63-590e175a262f";
            const CHARACTERISTIC_UUID = "1b68902e-da10-40d8-a58f-c68da82c3021";
            var ble = false; // BluetoothRemoteGATTCharacteristicオブジェクトがはいる

            // BLEデバイス検出
            function getBleDevice(jqObj) {
                jqObj.text('BLEデバイスを検索しています...');
                navigator.bluetooth.requestDevice({
                    acceptAllDevices:true,
                    optionalServices:[CUSTOM_SERVICE_UUID]
                }).then(device => {
                    // 接続します
                    jqObj.text('BLEデバイスに接続しています...');
                    console.log('Name:      ' + device.name);
                    console.log('Id:        ' + device.id);
                    console.log('Connected: ' + device.gatt.connected);
                    return device.gatt.connect();
                }).then(server =>{
                    // サービスの指定
                    jqObj.text('サービスを検索しています...');
                    console.log('-> getPrimaryService() ..');
                    return server.getPrimaryService(CUSTOM_SERVICE_UUID);
                }).then(service =>{
                    // キャラクタリスティックの指定
                    jqObj.text('キャラクタリスティックを検索しています...');
                    console.log('-> getCharacteristic() ..');
                    return service.getCharacteristic(CHARACTERISTIC_UUID);
                }).then(characteristic => {
                    // ここまできたら接続はOK
                    jqObj.text('接続されました'); // 開始ボタンの文字列を変更
                    console.log(characteristic);
                    ble = characteristic;
                    return characteristic;
                }).catch(error => {
                    console.log('[Error] ' + error);
                    if (!device.gatt.connected) {
                        jqObj.text('接続失敗');
                    }
                    return false;
                });
            }

            // ボタンクリックで開始
            $('#btn_start').click(function(e) {
                getBleDevice($(this));
            });

            // ブラウザからの操作
            $('.btn_tests').click(function(e) {
                if (ble) {
                    // 接続済み
                    var pattern = $(this).val().charAt(0);
                    var onoff = $(this).val().charAt(1);

                    // 切り替え
                    var btn_str;
                    if (onoff == '0') {
                        onoff = 1;
                        btn_str = pattern + ' : ■□';
                    } else {
                        onoff = 0;
                        btn_str = pattern + ' : □■';
                    }

                    // ボタン表示変更とvalue値の更新
                    $(this).text(btn_str);
                    var newval = pattern + onoff;
                    $(this).val(newval);

                    // BLE送信
                    ble.writeValue((new TextEncoder).encode(newval));
                } else {
                    // 未接続
                    $(this).text('未接続');
                }
            });

            // 全消灯
            $('#btn_all_off').click(function(e) {
                $('#btn_A').text('A : off');
                $('#btn_B').text('B : off');
                $('#btn_C').text('C : off');
                // BLE送信
                ble.writeValue((new TextEncoder).encode('X0'));
            });
        </script>

    </body>
</html>

jQuery下手くそなのですでにわかりづらいですが、
ポイントは
1. サービスとキャラクタリスティックのUUIDは予め生成しておき、Nefry BTに書き込んだものと同じにしておく。
2. Promiseを使って、デバイス検索→サービス→キャラクタリスティック→接続完了、と順番に進める。
3. 接続完了後、ブラウザボタンクリックで各LEDケーブルの電流方向をトグルする。
4. ble.writeValue((new TextEncoder).encode('文字列'))でデータ送信。TextEncoderでバイト列に変換する必要がある。

というあたりですね。
WebBluetoothに関しても素人なので以下の記事を見ながらJSを書きました。
参考: 話題のWebBluetoothでGenuino101をブラウザからリモートコントロールしてみよう | dotstudio

できた

ChristmasTree + NefryBT + WebBluetooth - YouTube

Nefry BTはパソコンにつないでいますが、電源供給とシリアルでのデバッグ出力のみに使っています。
Web上でNefryBT(X'mas)に接続し、ボタンをクリックすることでLEDが反応しているのがわかります。シリアルモニタでは、BLE受信した2文字のコマンドが表示されているのが確認できます。

ブラウザ上でのマウスの動きをツリーの点滅に反映させる

ボタンをぽちぽちクリックしているだけだと直感的でないので、マウスを動かしまくったら激しく点滅してくれるように、HTML側を改良しました。

ble2.html
<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <title>WebBluetooth - NefryBT - X'masTree (2)</title>
        <meta name="description" content="北京ダック">
        <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
    </head>

    <body>
        <p>ブラウザ上のマウスの動きでクリスマスツリーのLEDを制御する</p>
        <button id="btn_ble_ctrl">接続</button>
        <hr>
        X:<input type="text" id="mouse_x"> Diff:<input type="text" id="mouse_x_diff">
        <br>
        Y:<input type="text" id="mouse_y"> DIff:<input type="text" id="mouse_y_diff">

        <script>
            const CUSTOM_SERVICE_UUID = "2fb65514-1a38-4597-bf63-590e175a262f";
            const CHARACTERISTIC_UUID = "1b68902e-da10-40d8-a58f-c68da82c3021";
            var dev = false; // BluetoothRemoteGATTDevice?がはいるとおもう
            var ble = false; // BluetoothRemoteGATTCharacteristicオブジェクト
            var posX; // マウスX座標
            var posY; // マウスY座標
            var ledOff = true; // 全消灯かそうでないか

            // BLEデバイス検出
            function getBleDevice(jqObj) {
                jqObj.text('デバイス選択中...');
                navigator.bluetooth.requestDevice({
                    acceptAllDevices:true,
                    optionalServices:[CUSTOM_SERVICE_UUID]
                }).then(device => {
                    // 接続します
                    jqObj.text('接続中...');
                    console.log('Name:      ' + device.name);
                    console.log('Id:        ' + device.id);
                    console.log('Connected: ' + device.gatt.connected);
                    dev = device;
                    return device.gatt.connect();
                }).then(server =>{
                    // サービスの指定
                    jqObj.text('サービス検索中...');
                    console.log('-> getPrimaryService() ..');
                    return server.getPrimaryService(CUSTOM_SERVICE_UUID);
                }).then(service =>{
                    // キャラクタリスティックの指定
                    jqObj.text('キャラクタリスティック検索中...');
                    console.log('-> getCharacteristic() ..');
                    return service.getCharacteristic(CHARACTERISTIC_UUID);
                }).then(characteristic => {
                    // ここまできたら接続はOK
                    jqObj.text('切断'); // 開始ボタンの文字列を変更
                    console.log(characteristic);
                    ble = characteristic;
                    ble.writeValue((new TextEncoder).encode('X0')); // リセット送信
                    return characteristic;
                }).catch(error => {
                    console.log('[Error] ' + error);
                    if (!device.gatt.connected) {
                        jqObj.text('接続失敗');
                    }
                    return false;
                });
            }

            // ボタンクリックで接続または切断
            $('#btn_ble_ctrl').click(function(e) {
                if (ble) {
                    // 接続済->切断
                    ble.writeValue((new TextEncoder).encode('X0')); // リセット送信
                    $(this).text('接続'); // 次回接続用
                    ble = false;
                    dev.gatt.disconnect(); // 切断処理
                } else {
                    // 未接続->接続
                    getBleDevice($(this));
                }
            });

            // マウス位置取得
            $(function(){
                $(window).mousemove(function() {
                    // 差分取得
                    var diffX = posX-event.clientX;
                    var diffY = posY-event.clientY;

                    // 接続済みのときだけ送る
                    if (ble) {
                        // Aチャンネル
                        if (diffX >= 10) {
                            ble.writeValue((new TextEncoder).encode('A0')); // 送信
                            ledOff = false;
                        } else if (diffX <= -10) {
                            ble.writeValue((new TextEncoder).encode('A1')); // 送信
                            ledOff = false;
                        }
                        // Bチャンネル
                        if (diffY >= 10) {
                            ble.writeValue((new TextEncoder).encode('B0')); // 送信
                            ledOff = false;
                        } else if (diffY <= -10) {
                            ble.writeValue((new TextEncoder).encode('B1')); // 送信
                            ledOff = false;
                        }
                        // Cチャンネル
                        if (diffX * diffY >= 50) {
                            ble.writeValue((new TextEncoder).encode('C0')); // 送信
                            ledOff = false;
                        } else if (diffX * diffY <= -50) {
                            ble.writeValue((new TextEncoder).encode('C1')); // 送信
                            ledOff = false;
                        } else {
                            // いずれかのLEDがOnで移動量が規定未満のときに1回だけOffを送る
                            if (!ledOff) {
                                ble.writeValue((new TextEncoder).encode('X0'));
                                ledOff = true;
                            }
                        }
                    }

                    // ブラウザ側の表示を更新
                    $('#mouse_x_diff').val(diffX);
                    $('#mouse_y_diff').val(diffY);
                    $('#mouse_x').val(event.clientX);
                    $('#mouse_y').val(event.clientY);
                    posX = event.clientX;
                    posY = event.clientY;
                });
            });

        </script>

    </body>
</html>

ChristmasTree + NefryBT + WebBluetooth (ver.2) - YouTube

なかなか反応がいいですね🙃
あとはこれを、本来の「WebRTC」と組み合わせることができれば……

後編につづく!!

ukk0
https://xor.hateblo.jp/ < こっちに書いてます。アドベントカレンダー以外ここ使わないと思う。
https://twitter.com/ukokq
dotstudio
全ての人がモノづくりを楽しむ世界を目指して活動しています。
https://dotstud.io
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした