Node.js
Arduino
Socket.io
IoT
Johnny-Five

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

狙い

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

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

構成・仕様

ハードウェア

ソフトウェア

通信環境

  • PC(自宅)側
    • au ひかり
    • ホームゲートウェイ(NEC Aterm BL170HV
      • TCP のポート 80 と 443 を PC にポートマッピング(方法
        • PC の IP アドレスは固定しておく
      • MyDNS.JP でダイナミック DNS 登録(***.mydns.jp でつながる)
        • またはホームゲートウェイの WAN 側 IP アドレスを調べておく
    • 無線 LAN ルータ(Buffalo WXR-1900DHP2、ブリッジモード)
  • スマホ側
    • 楽天モバイル SIM(DoCoMo 回線)

構成図

構成図.png

コード

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

サーバ側

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 送信
  });
});

クライアント側

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

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

まとめ

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