【Websocket X IoT】Node.js上でWebsocketネットワークを構築し、ラズパイ&Arduinoをシリアル通信でデータを受信してみる


2021/06/13
蛸壺の中の工作室|Node.js上でWebsocketネットワークを構築し、ラズパイ&Arduinoをシリアル通信でデータを受信してみる

本ブログでは以前からWifiなどの無線機能の付いたベアメタル機器(ラズパイなど)をNode.jsで実装したMQTTサーバー&クライアントで通信のやり取りをする方法を紹介しておりました。

MQTTでIoT装置を遠隔操作する場合には、その装置がLANネットワークに繋がっていれば何処からでも指令が送れたり・受け取れたりしたわけです。

MQTTネットワークのIoTシステム構築の大前提は
ネットワークに接続できることですが、そう世の中IoT機器全てにWiFi通信モジュールが標準搭載してあるわけではないですし、無線通信モジュール付きのモデルはまだまだ高価です。

一方で、Arduino Unoのようにシリアル通信が主体のベアメタルが未だ長い間ロングセラーでユーザーに選ばれ続けているのは、価格の安さだけではなく、仕組みがシンプルで使いやすく、消費電力も最小に抑えられて、軽量化にも優れているなどの様々な魅力があるからだと思います。

そんな基本的にシリアル通信しかできないベアメタルをIoT化するには、一度何処かの中継機に通信接続させて、その中継機を介した遠隔操作を行うことで可能になります。

合同会社タコスキングダム|蛸壺の中の工作室

ということで今回はArduino UnoをUSB接続させたLinux機(ラズパイ)にNode.jsでWebsocketサーバーを立てつつ、内部ではWebsocketで送られた信号をシリアル信号に相互に変換してくれるアプリケーションを作成してみます。


websokect-nodeのインストール

Websocketサーバーを立てる選択肢でいうとsocket.ioも有名ですが、今回はNode.jsで軽量に動作するwebsokect-nodeを使うことにします。

node.jsが使える環境であれば概ね動作しますが、手元の環境ではnode12で動作確認しています。

以下でnpmパッケージから一発導入できます。

            $ npm install websocket serialport
        
なおついでにこのブログ後半で使うためnode-serialportもインストールしておきます。


Websocketサーバーのテスト

先にwebsocket-nodeを使ってWebsocketサーバーの動作確認をやっておきます。

こちらでサンプルを公開されているものを発見したので有り難くベースコードとして修正・利用させていただくとしましょう。

合同会社タコスキングダム|蛸壺の中の工作室

上の図で示すように、サーバーとなるLinux機側(ここではRaspberryPi)に以下のjavascriptコードを
server.jsとして実行します。

            const webSocketsServerPort = 41337;
const webSocketServer = require('websocket').server;
const http = require('http');
const server = http.createServer((request, response) => {
    // Not important for us.
    // We're writing WebSocket server, not HTTP server
});
const clients = [];

server.listen(webSocketsServerPort, () => {
    console.log((new Date()) + " Server is listening on port " + webSocketsServerPort);
});

const wsServer = new webSocketServer({httpServer: server});

// Event if the server recieved a request from a listed client
wsServer.on('request', (request) => {
    console.log((new Date()) + ' Connection from origin ' + request.origin + '.');
    const connection = request.accept(null, request.origin);
    console.log((new Date()) + ' Connection accepted.');

    const index = clients.push(connection) - 1;

    // Event when the server recieved message from clients
    connection.on('message', (message) => {
        if (message.type === 'utf8') {
            console.log('Received Message: ' + message.utf8Data);
        } else if (message.type === 'binary') {
            console.log('Received Binary Message of ' + message.binaryData.length + ' bytes');
        }
    });

    // Event when an user disconnected
    connection.on('close', (connection) => {
        console.log((new Date()) + " Peer " + connection.remoteAddress + " disconnected.");
        // Remove an user from the list of connected clients
        clients.splice(index, 1);
    });
});
        
ではこのコードをビルドしてみます。

            $ node server.js
Wed Jun 09 2021 02:54:08 GMT+0000 (Coordinated Universal Time) Server is listening on port 41337
        
これでWebsocketサーバーが単独で起動しているようです。

Websocketサーバーのレスポンステスト

Websocketクライアントの実装をする前に、同じネットワーク上の別のクライアント側PCからこのWebsocketサーバー(手元の環境では192.168.0.123:41337)にアクセスしてみます。

合同会社タコスキングダム|蛸壺の中の工作室

クライアントからCurlでWebsocketサーバーのエンドポイントを叩いてレスポンスを確認するだけのやり方で試します。

こちらで議論されていましたのでそのまま使わせてもらうとして、

            $ curl -o - --http1.1 --include \
    --no-buffer \
    --header "Connection: Upgrade" \
    --header "Upgrade: websocket" \
    --header "Host: 192.168.0.123:41337" \
    --header "Origin: http://192.168.0.123:41337" \
    --header "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
    --header "Sec-WebSocket-Version: 13" \
    http://192.168.0.123:41337/
#👇レスポンス
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: qGEgH3En71di5rrssAZTmtRTyFk=
Origin: http://192.168.0.123:41337
�
        
謎のバイナリが一つ返ってきましたがサーバーからのレスポンスが確認できます。

サーバー側の端末のレスポンスを覗いてみると、

            $ node server.js
Wed Jun 09 2021 02:54:08 GMT+0000 (Coordinated Universal Time) Server is listening on port 41337
Wed Jun 09 2021 02:54:21 GMT+0000 (Coordinated Universal Time) Connection from origin http://192.168.0.123:41337.
Wed Jun 09 2021 02:54:21 GMT+0000 (Coordinated Universal Time) Connection accepted.
Wed Jun 09 2021 02:54:41 GMT+0000 (Coordinated Universal Time) Peer undefined disconnected.
        
確かにWebsocket越しにサーバー-クライアント間で通信が接続が確立されているようです。クライアントが送信しているのが空の信号ですので、サーバ側では何も処理されず、このコネクションは20秒でタイムアウトして消滅しているようです。

ということでCurl側で出来るのは接続確認までで、きちんとした信号を送受信しようと思うとクライアント側もwebsocket-nodeで実装する必要があります。

何かしらの信号がサーバー側から送られてきたほうがWebsocketクライアントの実装がやりやすいので、以下では先にサーバーと接続されているシリアルデバイスの通信を確認していきます。


WebsocketサーバーとSerialportの連結

Websocketネットワーク間の通信は確立してそうですので、Arduinoとシリアル通信を行うための実装を追加していきます。

合同会社タコスキングダム|蛸壺の中の工作室

さて、デバイス間のシリアル通信を構築するのにもコツがいるため、本稿の内容とは別の記事でその手順を紹介してきました。

Arduinoとラズパイとのシリアル通信の構築に関しては以下の記事にまとめました。

シリアル通信とは言っても製品によって通信方式が微妙に異なるので、異なった製品を組み合わせる場合には色々と注意が必要になります。

さらに別の記事として、(最近の)node-serialportの使い方を考慮した内容も以下のリンクで解説しています。

ここで紹介しているnode-serialportの実装をそのまま流用し、上記で作りかけていた
server.jsを拡張してみます。

            const webSocketsServerPort = 41337;
const webSocketServer = require('websocket').server;
const http = require('http');
const server = http.createServer((request, response) => {});
const clients = [];

server.listen(webSocketsServerPort, () => {
    console.log((new Date()) + " Server is listening on port " + webSocketsServerPort);
});

const wsServer = new webSocketServer({httpServer: server});

//👇シリアル通信用のライブラリ読み込み
const SerialPort = require('serialport');
const Readline = require('@serialport/parser-readline');

//👇ゲスト上(Arduino側)のSerialデバイスに名前を合せる
const portName = '/dev/ttyUSB0';
//👇接続先(Arduino)のシリアルモデムを考慮
const sp = new SerialPort(portName, {
    baudRate: 9600,
    dataBits: 8,
    parity: 'none',
    stopBits: 1,
    flowControl: false,
});
//👇パーサクラスを指定
const parser = sp.pipe(new Readline());

// Event if the server recieved a request from a listed client
wsServer.on('request', (request) => {
    console.log((new Date()) + ' Connection from origin ' + request.origin + '.');
    const connection = request.accept(null, request.origin);
    console.log((new Date()) + ' Connection accepted.');

    const index = clients.push(connection) - 1;

    // Event when the server recieved message from clients
    connection.on('message', (message) => {
        if (message.type === 'utf8') {
            console.log('Received Message: ' + message.utf8Data);
            //👇クライアントから受け取った文字列をデバイスへリダイレクト
            sp.write(message.utf8Data, (err) => {
                if (err) {
                    return console.log('Error: ', err.message);
                }
                console.log('Utf8 data written over uart.');
            })
        } else if (message.type === 'binary') {
            console.log('Received Binary Message of ' + message.binaryData.length + ' bytes');
            //👇クライアントから受け取ったバイナリをデバイスへリダイレクト
            sp.write(message.binaryData, (err) => {
                if (err) {
                    return console.log('Error: ', err.message);
                }
                console.log('Byte data written over uart.');
            })
        }
    });

    // Event when an user disconnected
    connection.on('close', (connection) => {
        console.log((new Date()) + " Peer " + connection.remoteAddress + " disconnected.");
        // Remove an user from the list of connected clients
        clients.splice(index, 1);
    });
});

//👇ポート開放時の初期化処理
sp.on('open', ()=> {
    console.log('Open Serialport');
});

//👇データ(Arduino > ラズパイ)の受信
parser.on('data', (inp_) => {
    try {
        console.log(`Uart Msg: ${inp_}`);
        for (let i=0; i < clients.length; i++) {
            //👇接続中のすべてのwsクライアントへメッセージ送信
            clients[i].sendUTF(`Uart Msg: ${inp_}`);
        };
    } catch(e) {
        return;
    }
});
        
前回の構成と同様にArduinoとラズパイのシリアル通信を確立させたのちに、再びサーバーを起動しますと、、、

            $ node server.js
Sun Jun 13 2021 05:26:13 GMT+0000 (Coordinated Universal Time) Server is listening on port 41337
Open Serialport
Uart Msg: LED ON
Uart Msg: LED OFF
Uart Msg: LED ON
Uart Msg: LED OFF
Uart Msg: LED ON
#...以下略
        
と前回の結果同様に期待通りこのWebsocketサーバーはArduinoからのシリアル信号を受け続けます。


Websocketクライアントの実装

では再びWebsocketクライアント側で利用する簡単なアプリの実装方法に話を戻します。

合同会社タコスキングダム|蛸壺の中の工作室

クライアント側の作り方も様々あると思いますが、今回はWebsocketAPIとNodejsアプリの2通りを紹介します。

①WebsocketAPIを使う

最近のモダンなブラウザであればWebsocketAPIに対応しているものがほとんどですので、以下のようなhtmlファイルを用意してブラウザで開くだけで即時Websocketクライアント化が可能です。

ここではネットワーク上でWebsocketサーバーが
192.168.0.123:41337で起動しているものとします。

            <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        #display {
            width: 500px;
            height: 100px;
            font-size: 2em;
        }
    </style>
</head>
<body>
    <textarea id="display"></textarea>
    <script>
        const ws = new WebSocket('ws://192.168.0.248:41337');
        const display = document.getElementById('display');
        let currentData = {};
        ws.onmessage = (event) => {display.value = `${event.data}`;}
    </script>
</body>
</html>
        
このファイルを保存してクライアントのPCの適当なブラウザから開くと以下の動画のように動作していることが分かります。

合同会社タコスキングダム|蛸壺の中の工作室

※左側がクライアント側のブラウザ、右が稼働中のサーバープログラムからの表示。

②Nodejsネイティブで使う

Websocketクライアントで使えるライブラリもかなりありますが、ここでもサーバー・クライアント両サイドで使えるwebsokect-nodeを使います。

詳しい使い方は公式のドキュメントを見ていただくとして、以下のように公式のサンプルをサーバーからのメッセージを受け取るだけに修正したコードを
client.jsとして利用してみます。

            const WebSocketClient = require('websocket').client;
const client = new WebSocketClient();

//👇接続エラー時のイベント
client.on('connectFailed', (err) => {
    console.log('Connect Error: ' + err.toString());
});

//👇接続中のイベント
client.on('connect', (connection) => {
    console.log('WebSocket Client Connected...');

    //👇接続時のエラー
    connection.on('error', (err) => {
        console.log("Connection Error: " + err.toString());
    });

    //👇サーバーからのメッセージ受信時
    connection.on('message', (message) => {
        if (message.type === 'utf8') {
            console.log(`Received UTF8: ${message.utf8Data}`);
        } else if (message.type === 'binary') {
            console.log(`Received Binary: ${message.binaryData.length} Bytes`);
        }
    });

    //👇切断時のイベント
    connection.on('close', () => {
        console.log('Client Connection Closed');
    });
});

client.connect('ws://192.168.0.123:41337/');
        
これをネットワーク上のクライアントPCからnode client.jsでWebsocketサーバーにして上げると、以下のようになります。

合同会社タコスキングダム|蛸壺の中の工作室

※左側がクライアント側(Nodejsアプリ)、右側がWebsocketサーバーのレスポンス。

こちらもきちんと動作していることが分かります。


まとめ

以上、小規模ネットワーク上でWebsocketサーバー・クライアントを一通り構築する方法を詳しく解説してまいりましたがいかがでしたでしょうか。

以前解説したMQTTネットワークと比べて、Websocketはウェブブラウザとの機能統合の相性が良く、映像や音声などの大規模なストリーミングにも向いています。

対してMQTTのようにトピックという仕組みのようにデバイス間のデータを細かく仕分ける技術は無いので、特定のクライアントへ信号の送受信を行う場合には独自にサブスクリプション機能を実装しないといけないかも知れませんが、接続するデバイスの数が少ないうちはあまり考慮せずともよいかも知れません。


参考サイト

Node.jsでSocket.ioを使わずにWebSocket

Node.js で車とシリアル通信する

記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

電子工作を身近に知っていただけるように、材料調達からDIYのハウツーまで気になったところをできるだけ細かく記事にしてブログ配信してます。