PC+Webカメラ+Arduino+Node.jsで作る遠隔計測・制御システム

# 狙い
Node.js, Socket.io, Arduino, Johnny-five などなど、IoT 的なことについて趣味で遊んできたことについて、それらのエッセンスを組み合わせたシンプルな遠隔計測・制御システムを作ってみました。

特に目新しい情報があるわけではありませんが、主に自分の記録のために。


# 構成・仕様

## ハードウェア
* PC（Lenovo IdeaPad U330p, Core i3 4010U, Windows 10 Home Ver.1703）
* PC 内蔵 Web カメラ または USB Web カメラ
* [Arduino Uno R3](https://www.switch-science.com/catalog/789/)
* [SeeedStudio Grove Starter Kit for Arduino](https://www.switch-science.com/catalog/1812/)
    * LED ソケットと LED のみ使用
    * LED は D3（PWM できる端子）に接続
* [USB A-B ケーブル](https://www.sanwa.co.jp/product/syohin.asp?code=KU-M08W&cate=1)
* スマホ（iPhone SE [iOS 11.0.2]）

## ソフトウェア
* [Win64 OpenSSL v1.1.0f](https://slproweb.com/products/Win32OpenSSL.html)（秘密鍵 server.key と自己証明書 server.crt を作成しておく）
* [Arduino IDE](https://www.arduino.cc/en/main/software)（Arduino に StandardFirmataPlus を書き込んでおく）
* [Node.js](https://nodejs.org/ja/) 6.11.2
* [express](http://expressjs.com/) 4.15.4
* [socket.io](https://socket.io/) 2.0.3
* [johnny-five](http://johnny-five.io/) 0.11.6
* 任意のテキストエディタ（[Visual Studio Code](https://code.visualstudio.com/) 等）
* PC側ブラウザ：[Google Chrome](https://www.google.co.jp/chrome/browser/desktop/index.html) 61.0.3163.100
* スマホ側ブラウザ：Safari

## 通信環境
* PC（自宅）側
    * au ひかり
    * ホームゲートウェイ（[NEC Aterm BL170HV](http://www.aterm.jp/kddi/170hv/)）
        * TCP のポート 80 と 443 を PC にポートマッピング（[方法](http://www.aterm.jp/function/guide8/model/170/k/guide/high/high_saver_f.html)）
            * PC の IP アドレスは固定しておく
        * [MyDNS.JP](https://www.mydns.jp/) でダイナミック DNS 登録（***.mydns.jp でつながる）
            * またはホームゲートウェイの WAN 側 IP アドレスを調べておく
    * 無線 LAN ルータ（[Buffalo WXR-1900DHP2](http://buffalo.jp/product/wireless-lan/ap/wxr-1900dhp2/)、ブリッジモード）
* スマホ側
    * 楽天モバイル SIM（DoCoMo 回線）

## 構成図

![構成図.png](https://qiita-image-store.s3.amazonaws.com/0/157570/20701b82-bb00-1f64-3f43-0fc7b3dc24fe.png)



# コード

* 以下のサーバ側コード（app.js）とクライアント側コード（index.html）を作成し、例えば C:\\Users\\(ユーザ名)\\app 等に配置
* 事前に用意した自己証明書（server.key と server.crt）を例えば C:\\Users\\(ユーザ名)\\ssl に配置

## サーバ側

```js:app.js
"use strict";                                   // 厳格モードにする
// express を使う
const app = require("express")();               // express アプリを作る
app.get("/", (req, res) => {                    // ルートへのアクセス要求があったら
  res.sendFile(__dirname + "/index.html");      // ルートにある index.html を配信
});
// https サーバを立てる
const fs = require("fs");                       // fs モジュールを使う
const opt = {                                   // SSL 認証のパラメータ
  key:  fs.readFileSync("../ssl/server.key"),   // 秘密鍵（事前に OpenSSL 等で準備）
  cert: fs.readFileSync("../ssl/server.crt"),   // 証明書（事前に OpenSSL 等で準備）
};
const svr = require("https").Server(opt, app);  // https サーバを立てる
svr.listen(443);                                // 443番ポートを listen
// ボード（Arduino等）の制御
const five = require("johnny-five");            // johnny-five モジュールを使う
const board = new five.Board({port:"COM3"});    // COM3 に接続した制御ボード（Arduino等）を取得
const pinLed = 3;                               // LED を接続したピン番号
let   led;                                      // LED オブジェクトを入れる箱
board.on("ready", () => {                       // 制御ボードの準備ができたら
  led = new five.Led(pinLed);                   // LED オブジェクトの生成
});
// socket の送受信
const io = require("socket.io")(svr);           // socket.io を https サーバに紐づける
io.on("connection", (socket) => {               // socket 接続があったら
  socket.on("value", (dat) => {                 // value という socket を受信したら
    if( !board.isReady ) { return; }            // 制御ボードの準備ができていなければ抜ける
    led.brightness(dat);                        // LED の明るさを設定
    socket.broadcast.emit("value", dat);        // データを送信元以外に socket 送信
  });
  socket.on("video", (dat) => {                 // video という socket を受信したら
    socket.broadcast.emit("video", dat);        // データ（映像）を送信元以外に socket 送信
  });
});
```


## クライアント側

```html:index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>遠隔計測・制御システム</title>
<style>
  #vid { display: none; }                               /* カメラ映像表示用の video 要素を非表示 */
  #cvs { display: none; }                               /* カメラ映像キャプチャ用の canvas 要素を非表示 */
</style>
</head>
<body>
<video  id="vid" autoplay></video>                      <!-- カメラの映像を表示する video 要素 -->
<canvas id="cvs" width="160" height="120"></canvas>     <!-- カメラの映像をキャプチャする canvas 要素 -->
<img    id="rmt" width="160" height="120">              <!-- 受信した映像を表示する img 要素 -->
<br>                                                    <!-- 見た目のための改行 -->
<input  id="sld" type="range" min="0" max="255">        <!-- 値を入力するスライダ（遠隔制御用） -->
<script src="/socket.io/socket.io.js"></script>         <!-- socket.io.js の読み込み -->
<script>
  // socket.io の準備
  const socket = io();
  // スライダの値の送信
  const sld = document.getElementById("sld");           // input 要素 (range) を取得
  sld.value = 0;                                        // スライダの値を 0 に初期化
  sld.addEventListener("input", () => {                 // スライダで値が入力されたら
    socket.emit("value", sld.value);                    // スライダの値を socket で送信
  });
  // スライダの値の受信
  socket.on("value", (dat) => {                         // value という socket を受信したら
    sld.value = dat;                                    // スライダの値を変える
  });
  // 映像の取得と送信
  const media = navigator.mediaDevices.getUserMedia({   // メディアデバイスの準備
    video: true,                                        // 映像を使う
    audio: false,                                       // 音声は使わない
  });
  media.then((stream) => {                              // メディアデバイスの準備ができたら
    const vid = document.getElementById("vid");         // video 要素の取得
    vid.srcObject = stream;                             // video 要素にメディアストリームを入れる
    vid.addEventListener("timeupdate", () => {          // video の内容が更新されたら
      const cvs = document.getElementById("cvs");       // canvas 要素の取得
      const ctx = cvs.getContext("2d");                 // canvas の context の取得
      ctx.drawImage(vid, 0, 0, cvs.width, cvs.height);  // canvas に video 要素の内容を描画
      socket.emit("video", cvs.toDataURL());            // canvas の内容を base64 にして socket で送信
    });
  });
  // 映像の受信
  socket.on("video", (dat) => {                         // video という socket を受信したら
    const rmt = document.getElementById("rmt");         // img 要素の取得
    rmt.src = dat;                                      // 映像を img 要素に表示
  });
</script>
</body>
</html>
```


# 動作例

* コマンドプロンプト または PowerShell にて、Node.js で app.js を実行

```
C:\Users\(ユーザ名)\app>node app.js
```

* PC の Chrome にて、__https://__ で localhost を開く
    * 「この接続ではプライバシーが保護されません」と出るが、自作のサービスなので接続
    * カメラへのアクセスを許可する
    * Web カメラ映像の配信が始まる

```
https://localhost
```

* スマホ（iOS 11 の Safari）にて、__https://__ で 自宅のホームゲートウェイの URL（ダイナミック DNS に登録した ドメイン名（例：***.mydns.jp）または WAN 側 IP アドレス）を開く
    * スマホは自宅の無線LANルータにつながない（Wi-Fi を OFF）
    * 「接続はプライベートではありません」と出るが、自作のサービスなので接続
    * スマホ側はカメラへのアクセスを許可しなくてよい

```
https://***.mydns.jp
```

* スマホで以下のように表示され、スライダを動かすと LED の明るさが変わる
    * 以下の写真では、Web カメラの前に電波時計を置いておき、リアルタイムの映像であることを確認しつつ、電波時計に付属の温度計と湿度計の値もチェック

![IMG_1021.png](https://qiita-image-store.s3.amazonaws.com/0/157570/d2e67980-37d7-802f-4607-22853d64fb37.png)



* さらに念入りに、自宅の無線 LAN ルータ電波の圏外まで外出して確認すると、満足感が高まる
* サービス終了は、PC のコマンドプロンプトで「Ctrl+c」を2回入力



# まとめ

このシステム自体は何も嬉しいことができませんが、これをベースに、いろいろ遊べると思います。
なお、ホームゲートウェイのポートマッピング（ポート開放）設定や、SSL 自己証明書の作成などを伴っていますから、参考にされる場合は自己責任でお願いします。